From 6b557b05b253ecbd12fddf1f7d7f5d883a07448e Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Fri, 24 Jul 2026 14:07:24 -0600 Subject: [PATCH 1/4] feat: extract wellknownApi to shared @forgerock/sdk-wellknown package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single canonical RTK Query instance used by davinci-client, journey-client, and oidc-client — prerequisite for shared Redux store cache deduplication. --- .../centralize-redux-store/facts-result.json | 109 ++++++++++ .../centralize-redux-store/facts-review.json | 103 ++++++++++ goals/centralize-redux-store/facts.md | 14 ++ goals/centralize-redux-store/facts.meta.json | 86 ++++++++ goals/centralize-redux-store/goal.md | 22 +++ .../interview-result.json | 53 +++++ goals/centralize-redux-store/interview.json | 113 +++++++++++ goals/centralize-redux-store/plan.md | 187 ++++++++++++++++++ packages/davinci-client/package.json | 1 + .../davinci-client/src/lib/client.store.ts | 2 +- .../src/lib/client.store.utils.ts | 4 +- .../davinci-client/src/lib/wellknown.api.ts | 57 ------ packages/davinci-client/tsconfig.json | 3 + packages/davinci-client/tsconfig.lib.json | 3 + packages/journey-client/package.json | 1 + .../journey-client/src/lib/client.store.ts | 2 +- .../src/lib/client.store.utils.ts | 4 +- .../journey-client/src/lib/wellknown.api.ts | 43 ---- packages/journey-client/tsconfig.lib.json | 3 + packages/oidc-client/package.json | 1 + packages/oidc-client/src/lib/client.store.ts | 4 +- .../oidc-client/src/lib/client.store.utils.ts | 4 +- packages/oidc-client/tsconfig.lib.json | 3 + .../sdk-effects/wellknown/eslint.config.mjs | 22 +++ packages/sdk-effects/wellknown/package.json | 41 ++++ packages/sdk-effects/wellknown/src/index.ts | 9 + .../wellknown}/src/lib/wellknown.api.ts | 22 ++- packages/sdk-effects/wellknown/tsconfig.json | 16 ++ .../sdk-effects/wellknown/tsconfig.lib.json | 37 ++++ .../sdk-effects/wellknown/tsconfig.spec.json | 41 ++++ packages/sdk-effects/wellknown/vite.config.ts | 43 ++++ pnpm-lock.yaml | 21 ++ tsconfig.json | 3 + 33 files changed, 958 insertions(+), 119 deletions(-) create mode 100644 goals/centralize-redux-store/facts-result.json create mode 100644 goals/centralize-redux-store/facts-review.json create mode 100644 goals/centralize-redux-store/facts.md create mode 100644 goals/centralize-redux-store/facts.meta.json create mode 100644 goals/centralize-redux-store/goal.md create mode 100644 goals/centralize-redux-store/interview-result.json create mode 100644 goals/centralize-redux-store/interview.json create mode 100644 goals/centralize-redux-store/plan.md delete mode 100644 packages/davinci-client/src/lib/wellknown.api.ts delete mode 100644 packages/journey-client/src/lib/wellknown.api.ts create mode 100644 packages/sdk-effects/wellknown/eslint.config.mjs create mode 100644 packages/sdk-effects/wellknown/package.json create mode 100644 packages/sdk-effects/wellknown/src/index.ts rename packages/{oidc-client => sdk-effects/wellknown}/src/lib/wellknown.api.ts (76%) create mode 100644 packages/sdk-effects/wellknown/tsconfig.json create mode 100644 packages/sdk-effects/wellknown/tsconfig.lib.json create mode 100644 packages/sdk-effects/wellknown/tsconfig.spec.json create mode 100644 packages/sdk-effects/wellknown/vite.config.ts diff --git a/goals/centralize-redux-store/facts-result.json b/goals/centralize-redux-store/facts-result.json new file mode 100644 index 0000000000..1628a1d9d7 --- /dev/null +++ b/goals/centralize-redux-store/facts-result.json @@ -0,0 +1,109 @@ +{ + "decision": "submitted", + "stage": "facts", + "result": { + "stage": "facts", + "title": "Centralize Redux Store Across Client Packages", + "goalSlug": "centralize-redux-store", + "facts": [ + { + "id": "fact-1", + "text": "`davinci()` returns an object with a `store` property typed as an opaque `SdkStore` handle (not the raw Redux `Store` type).", + "accepted": true, + "removed": false, + "automatedVerification": true, + "recommendedAutomatedVerification": true + }, + { + "id": "fact-2", + "text": "`journey()` returns an object with a `store` property typed as an opaque `SdkStore` handle.", + "accepted": true, + "removed": false, + "automatedVerification": true, + "recommendedAutomatedVerification": true + }, + { + "id": "fact-3", + "text": "`oidc()` accepts an optional second argument — an `SdkStore` — and uses it instead of creating a new store when provided.", + "accepted": true, + "removed": false, + "automatedVerification": true, + "recommendedAutomatedVerification": true + }, + { + "id": "fact-4", + "text": "When `oidc()` is called without a second argument, it creates its own store exactly as it does today (no behavioral change for standalone usage).", + "accepted": true, + "removed": false, + "automatedVerification": true, + "recommendedAutomatedVerification": true + }, + { + "id": "fact-5", + "text": "The davinci/journey store is pre-provisioned with oidc's reducers (`oidcApi`) so that when `oidc()` receives the shared store, it can dispatch and select from its own slice without any runtime injection.", + "accepted": false, + "removed": false, + "comment": "I don't think we want `oidc` pre-provisioned. We only want `oidc` when it's been used as the oidc client.", + "automatedVerification": true, + "recommendedAutomatedVerification": true + }, + { + "id": "fact-6", + "text": "`wellknownApi` is extracted from `davinci-client`, `journey-client`, and `oidc-client` into `sdk-effects` (or an appropriate shared effects package) so all three import the same instance.", + "accepted": true, + "removed": false, + "automatedVerification": true, + "recommendedAutomatedVerification": true + }, + { + "id": "fact-7", + "text": "When `oidc()` shares a store with `davinci()`, there is only one `wellknown` cache entry — a second call to the same wellknown URL returns from cache without a network request.", + "accepted": true, + "removed": false, + "automatedVerification": true, + "recommendedAutomatedVerification": true + }, + { + "id": "fact-8", + "text": "A single `store.subscribe()` call on the davinci/journey client fires for state changes from both the flow client and oidc client when a shared store is in use.", + "accepted": true, + "removed": false, + "automatedVerification": true, + "recommendedAutomatedVerification": true + }, + { + "id": "fact-9", + "text": "All existing call sites (`davinci({ config })`, `journey({ config })`, `oidc({ config })`) continue to work without modification.", + "accepted": true, + "removed": false, + "automatedVerification": true, + "recommendedAutomatedVerification": true + }, + { + "id": "fact-10", + "text": "TypeScript consumers cannot access internal Redux primitives (dispatch, getState, etc.) through the exposed `SdkStore` type — it is opaque by design.", + "accepted": true, + "removed": false, + "automatedVerification": true, + "recommendedAutomatedVerification": true + }, + { + "id": "fact-11", + "text": "The `requestMiddleware` and `logger` passed to `davinci()`/`journey()` are inherited by oidc when using a shared store — oidc does not need to re-declare them.", + "accepted": true, + "removed": false, + "automatedVerification": false, + "recommendedAutomatedVerification": false + }, + { + "id": "fact-12", + "text": "All affected packages pass `pnpm lint`, `pnpm build`, and `pnpm test` after the changes.", + "accepted": true, + "removed": false, + "automatedVerification": true, + "recommendedAutomatedVerification": true + } + ], + "factsMarkdown": "# Facts\n\n- `davinci()` returns an object with a `store` property typed as an opaque `SdkStore` handle (not the raw Redux `Store` type).\n- `journey()` returns an object with a `store` property typed as an opaque `SdkStore` handle.\n- `oidc()` accepts an optional second argument — an `SdkStore` — and uses it instead of creating a new store when provided.\n- When `oidc()` is called without a second argument, it creates its own store exactly as it does today (no behavioral change for standalone usage).\n- `wellknownApi` is extracted from `davinci-client`, `journey-client`, and `oidc-client` into `sdk-effects` (or an appropriate shared effects package) so all three import the same instance.\n- When `oidc()` shares a store with `davinci()`, there is only one `wellknown` cache entry — a second call to the same wellknown URL returns from cache without a network request.\n- A single `store.subscribe()` call on the davinci/journey client fires for state changes from both the flow client and oidc client when a shared store is in use.\n- All existing call sites (`davinci({ config })`, `journey({ config })`, `oidc({ config })`) continue to work without modification.\n- TypeScript consumers cannot access internal Redux primitives (dispatch, getState, etc.) through the exposed `SdkStore` type — it is opaque by design.\n- The `requestMiddleware` and `logger` passed to `davinci()`/`journey()` are inherited by oidc when using a shared store — oidc does not need to re-declare them.\n- All affected packages pass `pnpm lint`, `pnpm build`, and `pnpm test` after the changes." + } +} diff --git a/goals/centralize-redux-store/facts-review.json b/goals/centralize-redux-store/facts-review.json new file mode 100644 index 0000000000..d7653b6042 --- /dev/null +++ b/goals/centralize-redux-store/facts-review.json @@ -0,0 +1,103 @@ +{ + "stage": "facts", + "title": "Centralize Redux Store Across Client Packages", + "goalSlug": "centralize-redux-store", + "facts": [ + { + "id": "fact-1", + "text": "`davinci()` returns an object with a `store` property typed as an opaque `SdkStore` handle (not the raw Redux `Store` type).", + "accepted": false, + "removed": false, + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-2", + "text": "`journey()` returns an object with a `store` property typed as an opaque `SdkStore` handle.", + "accepted": false, + "removed": false, + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-3", + "text": "`oidc()` accepts an optional second argument — an `SdkStore` — and uses it instead of creating a new store when provided.", + "accepted": false, + "removed": false, + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-4", + "text": "When `oidc()` is called without a second argument, it creates its own store exactly as it does today (no behavioral change for standalone usage).", + "accepted": false, + "removed": false, + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-5", + "text": "The davinci/journey store is pre-provisioned with oidc's reducers (`oidcApi`) so that when `oidc()` receives the shared store, it can dispatch and select from its own slice without any runtime injection.", + "accepted": false, + "removed": false, + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-6", + "text": "`wellknownApi` is extracted from `davinci-client`, `journey-client`, and `oidc-client` into `sdk-effects` (or an appropriate shared effects package) so all three import the same instance.", + "accepted": false, + "removed": false, + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-7", + "text": "When `oidc()` shares a store with `davinci()`, there is only one `wellknown` cache entry — a second call to the same wellknown URL returns from cache without a network request.", + "accepted": false, + "removed": false, + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-8", + "text": "A single `store.subscribe()` call on the davinci/journey client fires for state changes from both the flow client and oidc client when a shared store is in use.", + "accepted": false, + "removed": false, + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-9", + "text": "All existing call sites (`davinci({ config })`, `journey({ config })`, `oidc({ config })`) continue to work without modification.", + "accepted": false, + "removed": false, + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-10", + "text": "TypeScript consumers cannot access internal Redux primitives (dispatch, getState, etc.) through the exposed `SdkStore` type — it is opaque by design.", + "accepted": false, + "removed": false, + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-11", + "text": "The `requestMiddleware` and `logger` passed to `davinci()`/`journey()` are inherited by oidc when using a shared store — oidc does not need to re-declare them.", + "accepted": false, + "removed": false, + "recommendedAutomatedVerification": false, + "automatedVerification": false + }, + { + "id": "fact-12", + "text": "All affected packages pass `pnpm lint`, `pnpm build`, and `pnpm test` after the changes.", + "accepted": false, + "removed": false, + "recommendedAutomatedVerification": true, + "automatedVerification": true + } + ] +} diff --git a/goals/centralize-redux-store/facts.md b/goals/centralize-redux-store/facts.md new file mode 100644 index 0000000000..49d466e6df --- /dev/null +++ b/goals/centralize-redux-store/facts.md @@ -0,0 +1,14 @@ +# Facts + +- `davinci()` returns an object with a `store` property typed as an opaque `SdkStore` handle (not the raw Redux `Store` type). +- `journey()` returns an object with a `store` property typed as an opaque `SdkStore` handle. +- `oidc()` accepts an optional second argument — an `SdkStore` — and uses it instead of creating a new store when provided. +- When `oidc()` is called without a second argument, it creates its own store exactly as it does today (no behavioral change for standalone usage). +- `oidc` lazily injects its own reducers (`oidcApi`) into the shared store at `oidc()` init time — the owning store (davinci/journey) does NOT pre-provision oidc reducers. +- `wellknownApi` is extracted from `davinci-client`, `journey-client`, and `oidc-client` into `sdk-effects` (or an appropriate shared effects package) so all three import the same instance. +- When `oidc()` shares a store with `davinci()`, there is only one `wellknown` cache entry — a second call to the same wellknown URL returns from cache without a network request. +- A single `store.subscribe()` call on the davinci/journey client fires for state changes from both the flow client and oidc client when a shared store is in use. +- All existing call sites (`davinci({ config })`, `journey({ config })`, `oidc({ config })`) continue to work without modification. +- TypeScript consumers cannot access internal Redux primitives (dispatch, getState, etc.) through the exposed `SdkStore` type — it is opaque by design. +- The `requestMiddleware` and `logger` passed to `davinci()`/`journey()` are inherited by oidc when using a shared store — oidc does not need to re-declare them. +- All affected packages pass `pnpm lint`, `pnpm build`, and `pnpm test` after the changes. diff --git a/goals/centralize-redux-store/facts.meta.json b/goals/centralize-redux-store/facts.meta.json new file mode 100644 index 0000000000..49896c66b7 --- /dev/null +++ b/goals/centralize-redux-store/facts.meta.json @@ -0,0 +1,86 @@ +[ + { + "id": "fact-1", + "text": "`davinci()` returns an object with a `store` property typed as an opaque `SdkStore` handle (not the raw Redux `Store` type).", + "comment": "", + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-2", + "text": "`journey()` returns an object with a `store` property typed as an opaque `SdkStore` handle.", + "comment": "", + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-3", + "text": "`oidc()` accepts an optional second argument — an `SdkStore` — and uses it instead of creating a new store when provided.", + "comment": "", + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-4", + "text": "When `oidc()` is called without a second argument, it creates its own store exactly as it does today (no behavioral change for standalone usage).", + "comment": "", + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-5-revised", + "text": "`oidc` lazily injects its own reducers (`oidcApi`) into the shared store at `oidc()` init time — the owning store (davinci/journey) does NOT pre-provision oidc reducers.", + "comment": "User rejected pre-provisioning: 'I don't think we want oidc pre-provisioned. We only want oidc when it's been used as the oidc client.'", + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-6", + "text": "`wellknownApi` is extracted from `davinci-client`, `journey-client`, and `oidc-client` into `sdk-effects` (or an appropriate shared effects package) so all three import the same instance.", + "comment": "", + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-7", + "text": "When `oidc()` shares a store with `davinci()`, there is only one `wellknown` cache entry — a second call to the same wellknown URL returns from cache without a network request.", + "comment": "", + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-8", + "text": "A single `store.subscribe()` call on the davinci/journey client fires for state changes from both the flow client and oidc client when a shared store is in use.", + "comment": "", + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-9", + "text": "All existing call sites (`davinci({ config })`, `journey({ config })`, `oidc({ config })`) continue to work without modification.", + "comment": "", + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-10", + "text": "TypeScript consumers cannot access internal Redux primitives (dispatch, getState, etc.) through the exposed `SdkStore` type — it is opaque by design.", + "comment": "", + "recommendedAutomatedVerification": true, + "automatedVerification": true + }, + { + "id": "fact-11", + "text": "The `requestMiddleware` and `logger` passed to `davinci()`/`journey()` are inherited by oidc when using a shared store — oidc does not need to re-declare them.", + "comment": "", + "recommendedAutomatedVerification": false, + "automatedVerification": false + }, + { + "id": "fact-12", + "text": "All affected packages pass `pnpm lint`, `pnpm build`, and `pnpm test` after the changes.", + "comment": "", + "recommendedAutomatedVerification": true, + "automatedVerification": true + } +] diff --git a/goals/centralize-redux-store/goal.md b/goals/centralize-redux-store/goal.md new file mode 100644 index 0000000000..fa4942d99d --- /dev/null +++ b/goals/centralize-redux-store/goal.md @@ -0,0 +1,22 @@ +# Goal: Centralize Redux Store Across Client Packages + +## Articulated Goal + +Expose an opaque `SdkStore` handle from `davinci()` and `journey()`, and allow `oidc()` to accept that handle as an optional second argument. When provided, `oidc` lazily injects its own reducers and middleware into the shared store rather than creating a new one — eliminating the dual-store problem when pairing oidc with a flow client. All existing call sites remain unchanged. + +## Shared Understanding + +See [`facts.md`](./facts.md) for the full list of accepted facts defining correct behavior. + +## Execution Plan + +See [`plan.md`](./plan.md) for the ordered steps, files touched, and verification commands. + +## Done Condition + +- `davinci()` and `journey()` return objects include a `store: SdkStore` property +- `oidc(opts, sharedStore?)` accepts the optional second argument and injects into it when provided +- `wellknownApi` lives in a new `@forgerock/sdk-wellknown` package imported by all three clients +- A shared store results in one wellknown cache entry and a single unified `subscribe` +- All existing call sites work without modification +- `pnpm nx run-many -t build lint test` passes across all affected packages diff --git a/goals/centralize-redux-store/interview-result.json b/goals/centralize-redux-store/interview-result.json new file mode 100644 index 0000000000..63fea5dd15 --- /dev/null +++ b/goals/centralize-redux-store/interview-result.json @@ -0,0 +1,53 @@ +{ + "decision": "submitted", + "stage": "interview", + "result": { + "stage": "interview", + "title": "Centralize Redux Store Across Client Packages", + "goalSlug": "centralize-redux-store", + "answers": [ + { + "questionId": "store-exposure", + "selectedOptionIds": ["opaque-property"], + "customAnswer": "", + "answer": "store property (opaque SdkStore type)", + "completed": true + }, + { + "questionId": "oidc-injection", + "selectedOptionIds": ["second-arg"], + "customAnswer": "", + "answer": "Second argument to oidc()", + "completed": true + }, + { + "questionId": "wellknown-dedup", + "selectedOptionIds": ["extract-now"], + "customAnswer": "", + "answer": "Extract wellknownApi to sdk-effects in this PR", + "completed": true + }, + { + "questionId": "reducer-injection-strategy", + "selectedOptionIds": ["pre-register"], + "customAnswer": "", + "answer": "Pre-register oidc reducers in davinci/journey store upfront", + "completed": true + }, + { + "questionId": "scope", + "selectedOptionIds": ["davinci", "journey", "oidc", "sdk-effects"], + "customAnswer": "", + "answer": "davinci-client\njourney-client\noidc-client\nsdk-effects (wellknown extraction)", + "completed": true + }, + { + "questionId": "breaking-changes", + "selectedOptionIds": ["minor-ok"], + "customAnswer": "", + "answer": "Minor breaking changes acceptable (semver minor)", + "completed": true + } + ] + } +} diff --git a/goals/centralize-redux-store/interview.json b/goals/centralize-redux-store/interview.json new file mode 100644 index 0000000000..78c7badb5c --- /dev/null +++ b/goals/centralize-redux-store/interview.json @@ -0,0 +1,113 @@ +{ + "stage": "interview", + "title": "Centralize Redux Store Across Client Packages", + "goalSlug": "centralize-redux-store", + "questions": [ + { + "id": "store-exposure", + "prompt": "How should `davinci()`/`journey()` expose the store to consumers?", + "description": "The store is currently fully private. The simplest approach is adding a `store` property to the returned client object. An alternative is a dedicated `getStore()` method. A third option is exposing only an opaque handle typed as `SdkStore` (not the full Redux `Store` type) to discourage direct Redux usage.", + "answerMode": "single-custom", + "recommendedAnswer": "Add a `store` property typed as an opaque `SdkStore` handle on the return object — additive, zero breaking changes, consumers can pass it to oidc() but can't easily misuse the raw Redux API.", + "recommendedOptionIds": ["opaque-property"], + "options": [ + { + "id": "opaque-property", + "label": "store property (opaque SdkStore type)" + }, + { + "id": "full-property", + "label": "store property (full Redux Store type)" + }, + { "id": "getter-method", "label": "getStore() method" } + ], + "required": true + }, + { + "id": "oidc-injection", + "prompt": "How should `oidc()` accept the shared store?", + "description": "Option A: add an optional `store` param to the existing config object — `oidc({ config, store: client.store })`. Option B: add it as a second argument — `oidc({ config }, client.store)`. The survey shows oidc() already takes a single destructured options bag.", + "answerMode": "single-custom", + "recommendedAnswer": "Optional `store` field inside the existing options object — consistent with how requestMiddleware and logger are passed today.", + "recommendedOptionIds": ["options-bag"], + "options": [ + { + "id": "options-bag", + "label": "Optional field in existing options object" + }, + { "id": "second-arg", "label": "Second argument to oidc()" } + ], + "required": true + }, + { + "id": "wellknown-dedup", + "prompt": "Should `wellknownApi` be deduplicated as part of this work?", + "description": "All three packages each define their own `wellknownApi` RTK Query slice with the same reducerPath ('wellknown') and endpoint. With a shared store, both registrations would collide on the same reducer key. We could: (A) keep them separate and let RTK Query's cache dedup on-the-fly (they share the same reducerPath so this actually works), (B) extract `wellknownApi` to `sdk-effects` so both packages import the same instance, or (C) defer this to a follow-up.", + "answerMode": "single-custom", + "recommendedAnswer": "Extract wellknownApi to sdk-effects as part of this PR — the same-key collision is a latent bug we'd hit immediately when testing shared store, and extraction is straightforward.", + "recommendedOptionIds": ["extract-now"], + "options": [ + { + "id": "extract-now", + "label": "Extract wellknownApi to sdk-effects in this PR" + }, + { + "id": "rtk-dedup", + "label": "Rely on RTK Query same-key dedup (same reducerPath)" + }, + { "id": "defer", "label": "Defer to a follow-up" } + ], + "required": true + }, + { + "id": "reducer-injection-strategy", + "prompt": "How should oidc inject its reducers into a shared store?", + "description": "RTK 2.0 ships `combineSlices` and lazy reducer injection. Two clean approaches: (A) use `store.dispatch(oidcApi.util.updateSubscriptionOptions(...))` + manually add reducer via `store.injectReducers` if RTK exposes it, or (B) design `createClientStore` / `createJourneyStore` to pre-register all possible reducers with their slices (oidc's included) so the store is fully wired before oidc() receives it. Option B is simpler but slightly over-provisions; Option A is lazy but requires RTK 2.0 `combineSlices`.", + "answerMode": "single-custom", + "recommendedAnswer": "Pre-register all reducers in the owning store (davinci/journey) — avoids runtime injection complexity, reducer keys don't collide (config/node/davinci/journeyReducer/oidc/wellknown are all unique), and the extra state is negligible.", + "recommendedOptionIds": ["pre-register"], + "options": [ + { + "id": "pre-register", + "label": "Pre-register oidc reducers in davinci/journey store upfront" + }, + { + "id": "lazy-inject", + "label": "Lazy inject via RTK combineSlices at oidc() init time" + } + ], + "required": true + }, + { + "id": "scope", + "prompt": "Which packages are in scope for this change?", + "description": "davinci-client and journey-client are the store owners; oidc-client is the joiner. protect and device-client don't use Redux stores currently.", + "answerMode": "multi-custom", + "recommendedAnswer": "davinci-client, journey-client, oidc-client only.", + "recommendedOptionIds": ["davinci", "journey", "oidc"], + "options": [ + { "id": "davinci", "label": "davinci-client" }, + { "id": "journey", "label": "journey-client" }, + { "id": "oidc", "label": "oidc-client" }, + { "id": "sdk-effects", "label": "sdk-effects (wellknown extraction)" } + ], + "required": true + }, + { + "id": "breaking-changes", + "prompt": "Are any breaking changes acceptable?", + "description": "Exposing `store` on the return object is purely additive. Adding `store?` to oidc's options bag is additive. The only potential breaking change is if `wellknownApi` is extracted to sdk-effects and packages previously imported it directly — but that's an internal import path change, not a public API change.", + "answerMode": "single", + "recommendedAnswer": "No breaking changes to public API — internal import path changes are acceptable.", + "recommendedOptionIds": ["no-breaking"], + "options": [ + { "id": "no-breaking", "label": "No breaking public API changes" }, + { + "id": "minor-ok", + "label": "Minor breaking changes acceptable (semver minor)" + } + ], + "required": true + } + ] +} diff --git a/goals/centralize-redux-store/plan.md b/goals/centralize-redux-store/plan.md new file mode 100644 index 0000000000..449aaea4fe --- /dev/null +++ b/goals/centralize-redux-store/plan.md @@ -0,0 +1,187 @@ +# Plan: Centralize Redux Store Across Client Packages + +## Solution Approach + +Migrate all three stores from `configureStore` + `combineReducers` to RTK 2.0 `combineSlices`, which enables lazy reducer injection. `davinci()` and `journey()` expose an opaque `SdkStore` handle on their return object. `oidc()` accepts that handle as an optional second argument and injects its own reducers into it at init time. `wellknownApi` is extracted to a new `sdk-effects/wellknown` package so all three clients share the same RTK Query instance — critical for cache deduplication on a shared store. + +## Ordered Steps + +### Step 1 — Extract `wellknownApi` to `@forgerock/sdk-wellknown` + +**Why first:** The shared store relies on a single `wellknownApi` instance. If each package keeps its own copy, `createApi` creates distinct RTK Query reducers with distinct internal state — sharing the store won't deduplicate the cache. This must land before any store changes. + +**Files to create:** + +- `packages/sdk-effects/wellknown/package.json` — follow `packages/sdk-effects/logger/package.json` as template, name `@forgerock/sdk-wellknown` +- `packages/sdk-effects/wellknown/src/index.ts` — re-exports from `lib/wellknown.api.ts` +- `packages/sdk-effects/wellknown/src/lib/wellknown.api.ts` — canonical implementation (merge `davinci-client`'s version + oidc's `wellknownSelector`/`createWellknownSelector`) +- `packages/sdk-effects/wellknown/vite.config.ts` — copy from `logger/vite.config.ts` +- `packages/sdk-effects/wellknown/project.json` — Nx project config +- `packages/sdk-effects/wellknown/tsconfig.json` / `tsconfig.lib.json` / `tsconfig.spec.json` + +**Files to modify:** + +- `packages/davinci-client/package.json` — add `@forgerock/sdk-wellknown: workspace:*` dep +- `packages/journey-client/package.json` — same +- `packages/oidc-client/package.json` — same +- `packages/davinci-client/src/lib/wellknown.api.ts` — delete file, update all imports to `@forgerock/sdk-wellknown` +- `packages/journey-client/src/lib/wellknown.api.ts` — delete file, update all imports +- `packages/oidc-client/src/lib/wellknown.api.ts` — delete file, update all imports (note: oidc has `wellknownSelector` — move to the shared package) + +**Verification:** + +```bash +pnpm nx build @forgerock/sdk-wellknown +pnpm nx build @forgerock/davinci-client +pnpm nx build @forgerock/journey-client +pnpm nx build @forgerock/oidc-client +pnpm nx test @forgerock/davinci-client +pnpm nx test @forgerock/journey-client +pnpm nx test @forgerock/oidc-client +``` + +--- + +### Step 2 — Introduce `SdkStore` opaque type in a shared location + +**Files to create/modify:** + +- `packages/sdk-types/src/lib/store.types.ts` — define the opaque `SdkStore` interface: + + ```ts + // Opaque handle — consumers can pass it around but cannot access Redux internals + export interface SdkStore { + readonly __brand: unique symbol; + } + ``` + + Internally, the concrete type will extend this. Consumers only ever see `SdkStore`. + +- `packages/sdk-types/src/index.ts` — export `SdkStore` + +**Why here:** `sdk-types` is the shared contracts layer with no runtime code. `SdkStore` is purely a type contract between the packages. Keeping it here avoids a circular dependency (davinci → sdk-types is already valid; oidc → sdk-types is already valid). + +**Verification:** + +```bash +pnpm nx build @forgerock/sdk-types +``` + +--- + +### Step 3 — Migrate `davinci-client` store to `combineSlices` + expose `SdkStore` + +**Files to modify:** + +- `packages/davinci-client/src/lib/client.store.utils.ts`: + - Replace `configureStore` + inline `reducer: {}` with `combineSlices(configSlice, nodeSlice, davinciApi, wellknownApi).withLazyLoadedSlices()` + - Create `const dynamicMiddleware = createDynamicMiddleware()` and add `dynamicMiddleware.middleware` to the `configureStore` middleware chain alongside `davinciApi.middleware` and `wellknownApi.middleware` + - Attach `__dynamicMiddleware: dynamicMiddleware` to the store (or carry it alongside) so `oidc()` can call `addMiddleware` at inject time + - Export the `rootReducer` so the store can be typed + - Export `InjectableStore` type (internal, extends the RTK store type, carries `__dynamicMiddleware` and the `combineSlices` `.inject()` method) +- `packages/davinci-client/src/lib/client.store.ts`: + - Add `store: store as SdkStore` to the return object of `davinci()` + - The local `store` variable remains typed as `InjectableStore` — the cast to `SdkStore` happens only at the return boundary + +- `packages/davinci-client/src/types.ts` — export `DavinciClient` type update picks up `store: SdkStore` automatically via `Awaited>` + +**Verification:** + +```bash +pnpm nx build @forgerock/davinci-client +pnpm nx test @forgerock/davinci-client +# Type check: verify DavinciClient has store: SdkStore +# Type check: verify store.dispatch / store.getState are NOT accessible on SdkStore +``` + +--- + +### Step 4 — Migrate `journey-client` store to `combineSlices` + expose `SdkStore` + +Mirrors Step 3. + +**Files to modify:** + +- `packages/journey-client/src/lib/client.store.utils.ts` — same `combineSlices` migration +- `packages/journey-client/src/lib/client.store.ts` — add `store: store as SdkStore` to return object +- `packages/journey-client/src/types.ts` (or `index.ts`) — `JourneyClient` interface gains `store: SdkStore` + +**Verification:** + +```bash +pnpm nx build @forgerock/journey-client +pnpm nx test @forgerock/journey-client +``` + +--- + +### Step 5 — Migrate `oidc-client` to accept optional `SdkStore` second argument + lazy inject + +**Files to modify:** + +- `packages/oidc-client/src/lib/client.store.utils.ts`: + - Keep `createClientStore` for standalone usage (no change) + - Add `injectIntoStore(store: SdkStore): void` helper — casts back to `InjectableStore`, calls `.inject(oidcApi)` and `.inject(wellknownApi)` + +- `packages/oidc-client/src/lib/client.store.ts` — `oidc()` signature becomes: + + ```ts + export async function oidc(options: OidcOptions, sharedStore?: SdkStore); + ``` + + Inside: + - If `sharedStore` is provided: call `injectIntoStore(sharedStore)`, use `sharedStore` as the internal store (cast to `InjectableStore`) + - If not provided: call `createClientStore(...)` as today + - All methods below are unchanged — they close over `store`, so the injection is transparent + +- `packages/oidc-client/src/types.ts` — `OidcClient` type picks up automatically; export `SdkStore` re-export for consumer convenience + +**Verification:** + +```bash +pnpm nx build @forgerock/oidc-client +pnpm nx test @forgerock/oidc-client +# Integration test: create davinci() → pass client.store to oidc() → assert single wellknown fetch +``` + +--- + +### Step 6 — Write integration tests proving shared-store behavior + +**Files to create/modify:** + +- `packages/oidc-client/src/lib/shared-store.test.ts` (or add to existing integration test file): + - Test: `oidc(opts, davinciClient.store)` — `wellknownApi` cache has exactly one entry, no second network call + - Test: `store.subscribe` on davinci client fires when oidc dispatches + - Test: `oidc(opts)` standalone still creates its own store (existing behavior) + - Test: TypeScript compile-time: `davinciClient.store.dispatch` should be a type error + +**Verification:** + +```bash +pnpm nx test @forgerock/oidc-client +pnpm nx test @forgerock/davinci-client +``` + +--- + +### Step 7 — Verify full suite + type check + +```bash +pnpm nx run-many -t build --no-agents +pnpm nx run-many -t lint +pnpm nx run-many -t test +pnpm tsc --noEmit # or pnpm nx run-many -t typecheck +``` + +--- + +## Risks and Open Questions + +1. **RTK Query middleware — use `createDynamicMiddleware`**: RTK Query API slices need their `.middleware` in the store's middleware chain. For lazy-injected slices like `oidcApi`, we use RTK 2.0's `createDynamicMiddleware`. The owning store (davinci/journey) registers `dynamicMiddleware.middleware` in `configureStore`; the `DynamicMiddlewareInstance` is included in the `InjectableStore` internal type. When `oidc()` injects its reducers via `combineSlices`, it also calls `store.__dynamicMiddleware.addMiddleware(oidcApi.middleware)`. No downside — this is RTK's designed use case for lazy middleware registration. + +2. **`wellknownApi` selector `RootState` coupling**: `oidc-client`'s `wellknownSelector` is typed against oidc's own `RootState`. After extraction to `sdk-wellknown`, it needs a generic state type or the `wellknown` slice state shape directly. Use `wellknownApi.endpoints.configuration.select(url)` directly — it returns a state selector that only requires the `wellknown` key, not a full `RootState`. + +3. **`SdkStore` cast fidelity**: The internal `InjectableStore` → `SdkStore` cast must be verifiably safe. Add a helper that asserts the store has `inject` available at runtime before accepting it in `oidc()`. + +4. **Package creation friction**: Creating a new `sdk-effects/wellknown` sub-package requires Nx project registration. Check `packages/sdk-effects/logger/project.json` and `nx.json` for the exact scaffolding pattern to avoid CI graph errors. diff --git a/packages/davinci-client/package.json b/packages/davinci-client/package.json index 77af118fcb..4b738e5816 100644 --- a/packages/davinci-client/package.json +++ b/packages/davinci-client/package.json @@ -31,6 +31,7 @@ "@forgerock/sdk-request-middleware": "workspace:*", "@forgerock/sdk-types": "workspace:*", "@forgerock/sdk-utilities": "workspace:*", + "@forgerock/sdk-wellknown": "workspace:*", "@forgerock/storage": "workspace:*", "@reduxjs/toolkit": "catalog:", "effect": "catalog:effect", diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index a95401dd91..c383218176 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -23,7 +23,7 @@ import { pollingµ, getPollingModeµ } from './client.store.effects.js'; import { nodeSlice } from './node.slice.js'; import { davinciApi } from './davinci.api.js'; import { configSlice } from './config.slice.js'; -import { wellknownApi } from './wellknown.api.js'; +import { wellknownApi } from '@forgerock/sdk-wellknown'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; /** diff --git a/packages/davinci-client/src/lib/client.store.utils.ts b/packages/davinci-client/src/lib/client.store.utils.ts index 32f265c45a..585bef59e0 100644 --- a/packages/davinci-client/src/lib/client.store.utils.ts +++ b/packages/davinci-client/src/lib/client.store.utils.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -16,7 +16,7 @@ import type { InternalErrorResponse } from './client.types.js'; import { configSlice } from './config.slice.js'; import { nodeSlice } from './node.slice.js'; import { davinciApi } from './davinci.api.js'; -import { wellknownApi } from './wellknown.api.js'; +import { wellknownApi } from '@forgerock/sdk-wellknown'; export function createClientStore({ requestMiddleware, diff --git a/packages/davinci-client/src/lib/wellknown.api.ts b/packages/davinci-client/src/lib/wellknown.api.ts deleted file mode 100644 index 251d24a04d..0000000000 --- a/packages/davinci-client/src/lib/wellknown.api.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -import { createSelector } from '@reduxjs/toolkit'; -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; -import { initWellknownQuery } from '@forgerock/sdk-oidc'; - -import type { WellknownResponse } from '@forgerock/sdk-types'; -import type { - FetchBaseQueryError, - FetchBaseQueryMeta, - QueryReturnValue, -} from '@reduxjs/toolkit/query'; - -/** - * RTK Query API for well-known endpoint discovery. - * - * Uses the `initWellknownQuery` builder pattern from `@forgerock/sdk-oidc`. - * The builder constructs the request and validates the response; - * `fetchBaseQuery` handles the HTTP transport through RTK Query's pipeline. - */ -export const wellknownApi = createApi({ - reducerPath: 'wellknown', - baseQuery: fetchBaseQuery(), - endpoints: (builder) => ({ - configuration: builder.query({ - queryFn: async (url, _api, _extra, baseQuery) => { - const result = await initWellknownQuery(url).applyQuery(async (req) => { - const queryResult = await baseQuery(req); - return queryResult as QueryReturnValue; - }); - return result as QueryReturnValue< - WellknownResponse, - FetchBaseQueryError, - FetchBaseQueryMeta - >; - }, - }), - }), -}); - -/** - * Creates a memoized selector for cached well-known data. - * - * @param wellknownUrl - The well-known endpoint URL used as the cache key - * @returns A memoized selector that extracts the WellknownResponse from state, or undefined if not yet fetched - */ -export function createWellknownSelector(wellknownUrl: string) { - return createSelector( - wellknownApi.endpoints.configuration.select(wellknownUrl), - (result) => result?.data, - ); -} diff --git a/packages/davinci-client/tsconfig.json b/packages/davinci-client/tsconfig.json index 141b4ebf5f..4b134f1186 100644 --- a/packages/davinci-client/tsconfig.json +++ b/packages/davinci-client/tsconfig.json @@ -23,6 +23,9 @@ { "path": "../sdk-effects/sdk-request-middleware" }, + { + "path": "../sdk-effects/wellknown" + }, { "path": "../sdk-effects/oidc" }, diff --git a/packages/davinci-client/tsconfig.lib.json b/packages/davinci-client/tsconfig.lib.json index 6c7bbdefef..c0645d65ac 100644 --- a/packages/davinci-client/tsconfig.lib.json +++ b/packages/davinci-client/tsconfig.lib.json @@ -48,6 +48,9 @@ }, { "path": "../sdk-effects/logger/tsconfig.lib.json" + }, + { + "path": "../sdk-effects/wellknown/tsconfig.lib.json" } ] } diff --git a/packages/journey-client/package.json b/packages/journey-client/package.json index 886639e90c..0f0bff0877 100644 --- a/packages/journey-client/package.json +++ b/packages/journey-client/package.json @@ -37,6 +37,7 @@ "@forgerock/sdk-request-middleware": "workspace:*", "@forgerock/sdk-types": "workspace:*", "@forgerock/sdk-utilities": "workspace:*", + "@forgerock/sdk-wellknown": "workspace:*", "@forgerock/storage": "workspace:*", "@reduxjs/toolkit": "catalog:", "effect": "catalog:effect", diff --git a/packages/journey-client/src/lib/client.store.ts b/packages/journey-client/src/lib/client.store.ts index 32c2689a64..24a56103f0 100644 --- a/packages/journey-client/src/lib/client.store.ts +++ b/packages/journey-client/src/lib/client.store.ts @@ -23,7 +23,7 @@ import { createStorage } from '@forgerock/storage'; import * as Either from 'effect/Either'; import { createJourneyObject, parseJourneyResponse } from './journey.utils.js'; import type { JourneyResult } from './journey.utils.js'; -import { wellknownApi } from './wellknown.api.js'; +import { wellknownApi } from '@forgerock/sdk-wellknown'; import type { JourneyStep } from './step.utils.js'; import type { JourneyClientConfig } from './config.types.js'; diff --git a/packages/journey-client/src/lib/client.store.utils.ts b/packages/journey-client/src/lib/client.store.utils.ts index 0e0b05c794..5588c66c42 100644 --- a/packages/journey-client/src/lib/client.store.utils.ts +++ b/packages/journey-client/src/lib/client.store.utils.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -11,7 +11,7 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { configSlice } from './config.slice.js'; import { journeyApi } from './journey.api.js'; -import { wellknownApi } from './wellknown.api.js'; +import { wellknownApi } from '@forgerock/sdk-wellknown'; const rootReducer = combineReducers({ [journeyApi.reducerPath]: journeyApi.reducer, diff --git a/packages/journey-client/src/lib/wellknown.api.ts b/packages/journey-client/src/lib/wellknown.api.ts deleted file mode 100644 index 2c1c41e5a2..0000000000 --- a/packages/journey-client/src/lib/wellknown.api.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; -import { initWellknownQuery } from '@forgerock/sdk-oidc'; - -import type { WellknownResponse } from '@forgerock/sdk-types'; -import type { - FetchBaseQueryError, - FetchBaseQueryMeta, - QueryReturnValue, -} from '@reduxjs/toolkit/query'; - -/** - * RTK Query API for well-known endpoint discovery. - * - * Uses the `initWellknownQuery` builder pattern from `@forgerock/sdk-oidc`. - * The builder constructs the request and validates the response; - * `fetchBaseQuery` handles the HTTP transport through RTK Query's pipeline. - */ -export const wellknownApi = createApi({ - reducerPath: 'wellknown', - baseQuery: fetchBaseQuery(), - endpoints: (builder) => ({ - configuration: builder.query({ - queryFn: async (url, _api, _extra, baseQuery) => { - const result = await initWellknownQuery(url).applyQuery(async (req) => { - const queryResult = await baseQuery(req); - return queryResult as QueryReturnValue; - }); - return result as QueryReturnValue< - WellknownResponse, - FetchBaseQueryError, - FetchBaseQueryMeta - >; - }, - }), - }), -}); diff --git a/packages/journey-client/tsconfig.lib.json b/packages/journey-client/tsconfig.lib.json index ca3f899b8d..ae23b14b06 100644 --- a/packages/journey-client/tsconfig.lib.json +++ b/packages/journey-client/tsconfig.lib.json @@ -35,6 +35,9 @@ }, { "path": "../sdk-effects/logger/tsconfig.lib.json" + }, + { + "path": "../sdk-effects/wellknown/tsconfig.lib.json" } ] } diff --git a/packages/oidc-client/package.json b/packages/oidc-client/package.json index bf154c54ca..fd04f77657 100644 --- a/packages/oidc-client/package.json +++ b/packages/oidc-client/package.json @@ -33,6 +33,7 @@ "@forgerock/sdk-request-middleware": "workspace:*", "@forgerock/sdk-types": "workspace:*", "@forgerock/sdk-utilities": "workspace:*", + "@forgerock/sdk-wellknown": "workspace:*", "@forgerock/storage": "workspace:*", "@reduxjs/toolkit": "catalog:", "effect": "catalog:effect", diff --git a/packages/oidc-client/src/lib/client.store.ts b/packages/oidc-client/src/lib/client.store.ts index b7824d3178..15ec19bfaa 100644 --- a/packages/oidc-client/src/lib/client.store.ts +++ b/packages/oidc-client/src/lib/client.store.ts @@ -1,5 +1,5 @@ /* - * Copyright © 2025 - 2026 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -18,7 +18,7 @@ import { isExpiryWithinThreshold } from './token.utils.js'; import { logoutµ } from './logout.request.js'; import { oidcApi } from './oidc.api.js'; import { sessionCheckNoneµ, sessionCheckIdTokenµ } from './session.micros.js'; -import { wellknownApi, wellknownSelector } from './wellknown.api.js'; +import { wellknownApi, wellknownSelector } from '@forgerock/sdk-wellknown'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { GenericError, GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; diff --git a/packages/oidc-client/src/lib/client.store.utils.ts b/packages/oidc-client/src/lib/client.store.utils.ts index f7c5f30792..c58d13b6ab 100644 --- a/packages/oidc-client/src/lib/client.store.utils.ts +++ b/packages/oidc-client/src/lib/client.store.utils.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -9,7 +9,7 @@ import { logger as loggerFn } from '@forgerock/sdk-logger'; import { configureStore, type SerializedError } from '@reduxjs/toolkit'; import { oidcApi } from './oidc.api.js'; -import { wellknownApi } from './wellknown.api.js'; +import { wellknownApi } from '@forgerock/sdk-wellknown'; import type { GenericError } from '@forgerock/sdk-types'; import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; diff --git a/packages/oidc-client/tsconfig.lib.json b/packages/oidc-client/tsconfig.lib.json index d689866941..920e9ccdc8 100644 --- a/packages/oidc-client/tsconfig.lib.json +++ b/packages/oidc-client/tsconfig.lib.json @@ -39,6 +39,9 @@ }, { "path": "../sdk-effects/iframe-manager/tsconfig.lib.json" + }, + { + "path": "../sdk-effects/wellknown/tsconfig.lib.json" } ], "exclude": [ diff --git a/packages/sdk-effects/wellknown/eslint.config.mjs b/packages/sdk-effects/wellknown/eslint.config.mjs new file mode 100644 index 0000000000..980b6a3e9d --- /dev/null +++ b/packages/sdk-effects/wellknown/eslint.config.mjs @@ -0,0 +1,22 @@ +import baseConfig from '../../../eslint.config.mjs'; + +export default [ + ...baseConfig, + { + files: ['**/*.json'], + rules: { + '@nx/dependency-checks': [ + 'warn', + { + ignoredFiles: [ + '{projectRoot}/eslint.config.{js,cjs,mjs}', + '{projectRoot}/vite.config.{js,ts,mjs,mts}', + ], + }, + ], + }, + languageOptions: { + parser: (await import('jsonc-eslint-parser')).default, + }, + }, +]; diff --git a/packages/sdk-effects/wellknown/package.json b/packages/sdk-effects/wellknown/package.json new file mode 100644 index 0000000000..18e9feff21 --- /dev/null +++ b/packages/sdk-effects/wellknown/package.json @@ -0,0 +1,41 @@ +{ + "name": "@forgerock/sdk-wellknown", + "version": "1.0.0", + "private": false, + "repository": { + "type": "git", + "url": "git+https://github.com/ForgeRock/ping-javascript-sdk.git", + "directory": "packages/sdk-effects/wellknown" + }, + "description": "Shared wellknown/OpenID Connect discovery API for the Ping JavaScript SDK", + "license": "MIT", + "author": "ForgeRock", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js", + "default": "./dist/src/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/src/index.js", + "module": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "pnpm nx nxBuild", + "lint": "pnpm nx nxLint", + "test": "pnpm nx nxTest", + "test:watch": "pnpm nx nxTest --watch" + }, + "dependencies": { + "@forgerock/sdk-oidc": "workspace:*", + "@forgerock/sdk-types": "workspace:*", + "@reduxjs/toolkit": "catalog:" + }, + "nx": { + "tags": ["scope:sdk-effects"] + } +} diff --git a/packages/sdk-effects/wellknown/src/index.ts b/packages/sdk-effects/wellknown/src/index.ts new file mode 100644 index 0000000000..04f4e2c35b --- /dev/null +++ b/packages/sdk-effects/wellknown/src/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +export { wellknownApi, wellknownSelector, createWellknownSelector } from './lib/wellknown.api.js'; +export type { WellknownState } from './lib/wellknown.api.js'; diff --git a/packages/oidc-client/src/lib/wellknown.api.ts b/packages/sdk-effects/wellknown/src/lib/wellknown.api.ts similarity index 76% rename from packages/oidc-client/src/lib/wellknown.api.ts rename to packages/sdk-effects/wellknown/src/lib/wellknown.api.ts index b4da332e53..e2f5f12074 100644 --- a/packages/oidc-client/src/lib/wellknown.api.ts +++ b/packages/sdk-effects/wellknown/src/lib/wellknown.api.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -15,7 +15,6 @@ import type { FetchBaseQueryMeta, QueryReturnValue, } from '@reduxjs/toolkit/query'; -import type { RootState } from './client.types.js'; /** * RTK Query API for well-known endpoint discovery. @@ -23,6 +22,9 @@ import type { RootState } from './client.types.js'; * Uses the `initWellknownQuery` builder pattern from `@forgerock/sdk-oidc`. * The builder constructs the request and validates the response; * `fetchBaseQuery` handles the HTTP transport through RTK Query's pipeline. + * + * This is the canonical single instance — all SDK client packages import from here + * so that a shared Redux store gets a single cache entry per URL. */ export const wellknownApi = createApi({ reducerPath: 'wellknown', @@ -44,6 +46,11 @@ export const wellknownApi = createApi({ }), }); +/** Minimum state shape required to use wellknown selectors. */ +export type WellknownState = { + [wellknownApi.reducerPath]: ReturnType; +}; + /** * Creates a memoized selector for cached well-known data. * @@ -58,19 +65,16 @@ export function createWellknownSelector(wellknownUrl: string) { } /** - * Convenience selector for oidc-client's RootState type. + * Convenience selector for any state that contains the wellknown slice. * * Unlike {@link createWellknownSelector}, this immediately evaluates the * selector against the provided state rather than returning a reusable selector. * * @param wellknownUrl - The well-known endpoint URL used as the cache key - * @param state - The oidc-client Redux root state + * @param state - Any Redux state that includes the wellknown slice * @returns The cached WellknownResponse or undefined if not yet fetched */ -export function wellknownSelector(wellknownUrl: string, state: RootState) { - const selector = createSelector( - wellknownApi.endpoints.configuration.select(wellknownUrl), - (result) => result?.data, - ); +export function wellknownSelector(wellknownUrl: string, state: S) { + const selector = createWellknownSelector(wellknownUrl); return selector(state); } diff --git a/packages/sdk-effects/wellknown/tsconfig.json b/packages/sdk-effects/wellknown/tsconfig.json new file mode 100644 index 0000000000..3a5af05d8e --- /dev/null +++ b/packages/sdk-effects/wellknown/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "nx": { + "addTypecheckTarget": false + } +} diff --git a/packages/sdk-effects/wellknown/tsconfig.lib.json b/packages/sdk-effects/wellknown/tsconfig.lib.json new file mode 100644 index 0000000000..3ded621972 --- /dev/null +++ b/packages/sdk-effects/wellknown/tsconfig.lib.json @@ -0,0 +1,37 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": false, + "module": "nodenext", + "moduleResolution": "nodenext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "references": [ + { "path": "../../sdk-types/tsconfig.lib.json" }, + { "path": "../oidc/tsconfig.lib.json" } + ], + "exclude": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" + ] +} diff --git a/packages/sdk-effects/wellknown/tsconfig.spec.json b/packages/sdk-effects/wellknown/tsconfig.spec.json new file mode 100644 index 0000000000..c8fc6b21fa --- /dev/null +++ b/packages/sdk-effects/wellknown/tsconfig.spec.json @@ -0,0 +1,41 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "module": "nodenext", + "moduleResolution": "nodenext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/sdk-effects/wellknown/vite.config.ts b/packages/sdk-effects/wellknown/vite.config.ts new file mode 100644 index 0000000000..dd352b8bbd --- /dev/null +++ b/packages/sdk-effects/wellknown/vite.config.ts @@ -0,0 +1,43 @@ +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/packages/effects/wellknown', + plugins: [], + test: { + watch: false, + globals: true, + environment: 'node', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + include: ['src/**/*.{js,ts}'], + exclude: [ + 'src/**/*.mock.{js,ts}', + 'src/**/*.data.{js,ts}', + 'src/**/*.test.{js,ts}', + 'coverage/**', + 'dist/**', + '**/node_modules/**', + '**/[.]**', + 'packages/*/test?(s)/**', + '**/*.d.ts', + '**/virtual:*', + '**/__x00__*', + '**/ *', + 'cypress/**', + 'test?(s)/**', + 'test?(-*).?(c|m)[jt]s?(x)', + '**/*{.,-}{test,spec,bench,benchmark}?(-d).?(c|m)[jt]s?(x)', + '**/__tests__/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*', + '**/vitest.{workspace,projects}.[jt]s?(on)', + '**/.{eslint,mocha,prettier}rc.{?(c|m)js,yml}', + ], + reporter: ['text', 'html', 'json'], + enabled: Boolean(process.env['CI']), + reportsDirectory: './coverage', + provider: 'v8' as const, + }, + }, +})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9cd3e060fe..d63d00b9b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -452,6 +452,9 @@ importers: '@forgerock/sdk-utilities': specifier: workspace:* version: link:../sdk-utilities + '@forgerock/sdk-wellknown': + specifier: workspace:* + version: link:../sdk-effects/wellknown '@forgerock/storage': specifier: workspace:* version: link:../sdk-effects/storage @@ -502,6 +505,9 @@ importers: '@forgerock/sdk-utilities': specifier: workspace:* version: link:../sdk-utilities + '@forgerock/sdk-wellknown': + specifier: workspace:* + version: link:../sdk-effects/wellknown '@forgerock/storage': specifier: workspace:* version: link:../sdk-effects/storage @@ -548,6 +554,9 @@ importers: '@forgerock/sdk-utilities': specifier: workspace:* version: link:../sdk-utilities + '@forgerock/sdk-wellknown': + specifier: workspace:* + version: link:../sdk-effects/wellknown '@forgerock/storage': specifier: workspace:* version: link:../sdk-effects/storage @@ -599,6 +608,18 @@ importers: specifier: workspace:* version: link:../../sdk-types + packages/sdk-effects/wellknown: + dependencies: + '@forgerock/sdk-oidc': + specifier: workspace:* + version: link:../oidc + '@forgerock/sdk-types': + specifier: workspace:* + version: link:../../sdk-types + '@reduxjs/toolkit': + specifier: 'catalog:' + version: 2.10.1 + packages/sdk-types: {} packages/sdk-utilities: diff --git a/tsconfig.json b/tsconfig.json index c22692ddac..699449c133 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -84,6 +84,9 @@ }, { "path": "./tools/api-report" + }, + { + "path": "./packages/sdk-effects/wellknown" } ] } From 5f8140d7fcde042db2790ed53aec6c065225ceb1 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Mon, 27 Jul 2026 12:32:11 -0600 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20centralize=20Redux=20stores=20?= =?UTF-8?q?=E2=80=94=20expose=20SdkStore=20from=20davinci/journey,=20injec?= =?UTF-8?q?t=20into=20oidc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit davinci() and journey() now expose an opaque SdkStore handle on their return value. oidc() accepts that handle as an optional second argument and lazily injects its reducer and middleware into the shared store via combineSlices.inject() and createDynamicMiddleware.addMiddleware(), so both clients share a single Redux store with one deduplicated wellknown cache entry. - sdk-types: add SdkStore opaque type (symbol brand) - sdk-wellknown: add passWithNoTests to vite.config.ts - davinci-client: migrate to combineSlices + InjectableStore + toSdkStore/fromSdkStore - journey-client: same migration pattern, expose store: SdkStore on JourneyClient - oidc-client: add injectIntoStore() helper; oidc(opts, sharedStore?) API - oidc-client: shared-store.test.ts — 7 integration tests covering injection contract and cache deduplication behaviour toSdkStore/fromSdkStore internal helpers use object type to avoid api-extractor isExternal/isInternal conflict on SdkStore; cast to SdkStore at the public boundary. --- .../api-report/davinci-client.api.md | 2 + .../api-report/davinci-client.types.api.md | 2 + .../src/lib/client.store.effects.ts | 6 +- .../davinci-client/src/lib/client.store.ts | 7 +- .../src/lib/client.store.utils.ts | 80 ++++-- .../api-report/journey-client.api.md | 3 + .../api-report/journey-client.types.api.md | 3 + .../journey-client/src/lib/client.store.ts | 9 +- .../src/lib/client.store.utils.ts | 52 +++- .../oidc-client/api-report/oidc-client.api.md | 3 +- .../api-report/oidc-client.types.api.md | 3 +- packages/oidc-client/src/lib/client.store.ts | 39 +-- .../oidc-client/src/lib/client.store.utils.ts | 31 +++ .../oidc-client/src/lib/shared-store.test.ts | 236 ++++++++++++++++++ packages/sdk-effects/wellknown/vite.config.ts | 1 + packages/sdk-types/src/index.ts | 3 +- packages/sdk-types/src/lib/store.types.ts | 16 ++ 17 files changed, 437 insertions(+), 59 deletions(-) create mode 100644 packages/oidc-client/src/lib/shared-store.test.ts create mode 100644 packages/sdk-types/src/lib/store.types.ts diff --git a/packages/davinci-client/api-report/davinci-client.api.md b/packages/davinci-client/api-report/davinci-client.api.md index ea01559bcd..aec288bc28 100644 --- a/packages/davinci-client/api-report/davinci-client.api.md +++ b/packages/davinci-client/api-report/davinci-client.api.md @@ -16,6 +16,7 @@ import type { MutationResultSelectorResult } from '@reduxjs/toolkit/query'; import { QueryStatus } from '@reduxjs/toolkit/query'; import { Reducer } from '@reduxjs/toolkit'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-types'; import { SerializedError } from '@reduxjs/toolkit'; import { Unsubscribe } from '@reduxjs/toolkit'; @@ -278,6 +279,7 @@ export function davinci(input: { custom?: CustomLogger; }; }): Promise<{ + store: SdkStore; subscribe: (listener: () => void) => Unsubscribe; externalIdp: () => (() => Promise); flow: (action: DaVinciAction) => InitFlow; diff --git a/packages/davinci-client/api-report/davinci-client.types.api.md b/packages/davinci-client/api-report/davinci-client.types.api.md index 4ae2da4a09..88f48561f2 100644 --- a/packages/davinci-client/api-report/davinci-client.types.api.md +++ b/packages/davinci-client/api-report/davinci-client.types.api.md @@ -16,6 +16,7 @@ import type { MutationResultSelectorResult } from '@reduxjs/toolkit/query'; import { QueryStatus } from '@reduxjs/toolkit/query'; import { Reducer } from '@reduxjs/toolkit'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-types'; import { SerializedError } from '@reduxjs/toolkit'; import { Unsubscribe } from '@reduxjs/toolkit'; @@ -278,6 +279,7 @@ export function davinci(input: { custom?: CustomLogger; }; }): Promise<{ + store: SdkStore; subscribe: (listener: () => void) => Unsubscribe; externalIdp: () => (() => Promise); flow: (action: DaVinciAction) => InitFlow; diff --git a/packages/davinci-client/src/lib/client.store.effects.ts b/packages/davinci-client/src/lib/client.store.effects.ts index 9923ea4b67..de97bc0c39 100644 --- a/packages/davinci-client/src/lib/client.store.effects.ts +++ b/packages/davinci-client/src/lib/client.store.effects.ts @@ -11,7 +11,7 @@ import { FetchBaseQueryError } from '@reduxjs/toolkit/query/react'; import type { logger as loggerFn } from '@forgerock/sdk-logger'; -import type { ClientStore, RootState } from './client.store.utils.js'; +import type { DavinciStore, RootState } from './client.store.utils.js'; import type { PollingStatus, InternalErrorResponse } from './client.types.js'; import type { PollingCollector } from './collector.types.js'; @@ -239,7 +239,7 @@ function challengePollingµ({ }: { collector: PollingCollector; challenge: string; - store: ReturnType; + store: DavinciStore; log: ReturnType; }): Micro.Micro { const maxRetries = collector.output.config.pollRetries ?? 60; @@ -295,7 +295,7 @@ export function pollingµ({ }: { mode: PollingMode; collector: PollingCollector; - store: ReturnType; + store: DavinciStore; log: ReturnType; }): Micro.Micro { if (mode._tag === 'challenge') { diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index c383218176..ee180a9cd8 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -17,6 +17,7 @@ import { createClientStore, createInternalError, handleUpdateValidateError, + toSdkStore, type RootState, } from './client.store.utils.js'; import { pollingµ, getPollingModeµ } from './client.store.effects.js'; @@ -26,6 +27,7 @@ import { configSlice } from './config.slice.js'; import { wellknownApi } from '@forgerock/sdk-wellknown'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-types'; /** * Import the DaVinciRequest types */ @@ -81,7 +83,8 @@ export async function davinci({ level: logger?.level ?? config.log ?? 'error', custom: logger?.custom, }); - const store = createClientStore({ requestMiddleware, logger: log }); + const injectable = createClientStore({ requestMiddleware, logger: log }); + const store = injectable.store; const serverInfo = createStorage({ type: 'localStorage', name: 'serverInfo', @@ -113,6 +116,8 @@ export async function davinci({ store.dispatch(configSlice.actions.set({ ...config, wellknownResponse: openIdResponse })); return { + // Opaque store handle — pass to oidc() to share this store + store: toSdkStore(injectable) as SdkStore, // Pass store methods to the client subscribe: store.subscribe, diff --git a/packages/davinci-client/src/lib/client.store.utils.ts b/packages/davinci-client/src/lib/client.store.utils.ts index 585bef59e0..21810cc7f9 100644 --- a/packages/davinci-client/src/lib/client.store.utils.ts +++ b/packages/davinci-client/src/lib/client.store.utils.ts @@ -4,7 +4,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { configureStore } from '@reduxjs/toolkit'; +import { combineSlices, configureStore, createDynamicMiddleware } from '@reduxjs/toolkit'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { logger as loggerFn } from '@forgerock/sdk-logger'; @@ -18,20 +18,48 @@ import { nodeSlice } from './node.slice.js'; import { davinciApi } from './davinci.api.js'; import { wellknownApi } from '@forgerock/sdk-wellknown'; +/** + * Root reducer built with combineSlices to support lazy injection. + * External slices (e.g. oidcApi) can be injected via rootReducer.inject(). + */ +export const rootReducer = combineSlices( + configSlice, + nodeSlice, + davinciApi, + wellknownApi, +).withLazyLoadedSlices(); + +export type RootState = ReturnType; + +export interface RootStateWithNode< + T extends ErrorNode | ContinueNode | StartNode | SuccessNode, +> extends RootState { + node: T; +} + +/** + * Internal store shape that carries the root reducer and dynamic middleware + * so that oidc-client can inject its reducers and middleware at init time. + * + * Consumers only ever see the opaque SdkStore type. + */ +export interface InjectableStore { + readonly store: ReturnType>; + readonly rootReducer: typeof rootReducer; + readonly dynamicMiddleware: ReturnType; +} + export function createClientStore({ requestMiddleware, logger, }: { requestMiddleware?: RequestMiddleware[]; logger?: ReturnType; -}) { - return configureStore({ - reducer: { - config: configSlice.reducer, - node: nodeSlice.reducer, - [davinciApi.reducerPath]: davinciApi.reducer, - [wellknownApi.reducerPath]: wellknownApi.reducer, - }, +}): InjectableStore { + const dynamicMiddleware = createDynamicMiddleware(); + + const store = configureStore({ + reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: { @@ -46,10 +74,20 @@ export function createClientStore({ }, }) .concat(davinciApi.middleware) - .concat(wellknownApi.middleware), + .concat(wellknownApi.middleware) + .concat(dynamicMiddleware.middleware), }); + + return { store, rootReducer, dynamicMiddleware }; } +export type ClientStore = typeof createClientStore; + +/** The inner Redux store type — used by effects that need dispatch/getState. */ +export type DavinciStore = InjectableStore['store']; + +export type AppDispatch = ReturnType; + export function handleUpdateValidateError( message: string, type: 'argument_error' | 'state_error', @@ -67,18 +105,6 @@ export function handleUpdateValidateError( }; } -export type ClientStore = typeof createClientStore; - -export type RootState = ReturnType['getState']>; - -export interface RootStateWithNode< - T extends ErrorNode | ContinueNode | StartNode | SuccessNode, -> extends RootState { - node: T; -} - -export type AppDispatch = ReturnType['dispatch']>; - /** * @function createInternalError * @description - Creates an InternalErrorResponse object @@ -104,3 +130,13 @@ export function isInternalError(value: unknown): value is InternalErrorResponse (value as Record)['type'] === 'internal_error' ); } + +/** Cast InjectableStore to the opaque SdkStore for public API exposure. */ +export function toSdkStore(injectable: InjectableStore): object { + return injectable as unknown as object; +} + +/** Recover the InjectableStore from an opaque SdkStore handle. */ +export function fromSdkStore(sdkStore: object): InjectableStore { + return sdkStore as unknown as InjectableStore; +} diff --git a/packages/journey-client/api-report/journey-client.api.md b/packages/journey-client/api-report/journey-client.api.md index 35a4a51dbe..88ee56f6fb 100644 --- a/packages/journey-client/api-report/journey-client.api.md +++ b/packages/journey-client/api-report/journey-client.api.md @@ -23,6 +23,7 @@ import { PolicyKey } from '@forgerock/sdk-types'; import { PolicyParams } from '@forgerock/sdk-types'; import { PolicyRequirement } from '@forgerock/sdk-types'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-types'; import { Step } from '@forgerock/sdk-types'; import { StepDetail } from '@forgerock/sdk-types'; import { StepType } from '@forgerock/sdk-types'; @@ -197,6 +198,8 @@ export interface JourneyClient { // (undocumented) start: (options?: StartParam) => Promise; // (undocumented) + store: SdkStore; + // (undocumented) subscribe: (listener: () => void) => () => void; // (undocumented) terminate: (options?: { diff --git a/packages/journey-client/api-report/journey-client.types.api.md b/packages/journey-client/api-report/journey-client.types.api.md index d9219a9710..340dadaa91 100644 --- a/packages/journey-client/api-report/journey-client.types.api.md +++ b/packages/journey-client/api-report/journey-client.types.api.md @@ -22,6 +22,7 @@ import { PolicyKey } from '@forgerock/sdk-types'; import { PolicyParams } from '@forgerock/sdk-types'; import { PolicyRequirement } from '@forgerock/sdk-types'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-types'; import { Step } from '@forgerock/sdk-types'; import { StepDetail } from '@forgerock/sdk-types'; import { StepType } from '@forgerock/sdk-types'; @@ -184,6 +185,8 @@ export interface JourneyClient { // (undocumented) start: (options?: StartParam) => Promise; // (undocumented) + store: SdkStore; + // (undocumented) subscribe: (listener: () => void) => () => void; // (undocumented) terminate: (options?: { diff --git a/packages/journey-client/src/lib/client.store.ts b/packages/journey-client/src/lib/client.store.ts index 24a56103f0..5ffa627c93 100644 --- a/packages/journey-client/src/lib/client.store.ts +++ b/packages/journey-client/src/lib/client.store.ts @@ -12,11 +12,11 @@ import { isValidWellknownUrl, createWellknownError, } from '@forgerock/sdk-utilities'; -import type { GenericError } from '@forgerock/sdk-types'; +import type { GenericError, SdkStore } from '@forgerock/sdk-types'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { Step } from '@forgerock/sdk-types'; -import { createJourneyStore } from './client.store.utils.js'; +import { createJourneyStore, toSdkStore } from './client.store.utils.js'; import { configSlice } from './config.slice.js'; import { journeyApi } from './journey.api.js'; import { createStorage } from '@forgerock/storage'; @@ -32,6 +32,7 @@ import type { NextOptions, StartParam, ResumeOptions } from './interfaces.js'; /** The journey client instance returned by the `journey()` function. */ export interface JourneyClient { + store: SdkStore; subscribe: (listener: () => void) => () => void; start: (options?: StartParam) => Promise; next: (step: JourneyStep, options?: NextOptions) => Promise; @@ -113,7 +114,8 @@ export async function journey({ ); } - const store = createJourneyStore({ requestMiddleware, logger: log }); + const injectable = createJourneyStore({ requestMiddleware, logger: log }); + const store = injectable.store; const { wellknown } = config.serverConfig; @@ -154,6 +156,7 @@ export async function journey({ }); const self: JourneyClient = { + store: toSdkStore(injectable) as SdkStore, subscribe: store.subscribe, start: async (options?: StartParam) => { diff --git a/packages/journey-client/src/lib/client.store.utils.ts b/packages/journey-client/src/lib/client.store.utils.ts index 5588c66c42..0298b20ff6 100644 --- a/packages/journey-client/src/lib/client.store.utils.ts +++ b/packages/journey-client/src/lib/client.store.utils.ts @@ -7,17 +7,36 @@ import { logger as loggerFn } from '@forgerock/sdk-logger'; import { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; -import { combineReducers, configureStore } from '@reduxjs/toolkit'; +import { combineSlices, configureStore, createDynamicMiddleware } from '@reduxjs/toolkit'; import { configSlice } from './config.slice.js'; import { journeyApi } from './journey.api.js'; import { wellknownApi } from '@forgerock/sdk-wellknown'; -const rootReducer = combineReducers({ - [journeyApi.reducerPath]: journeyApi.reducer, - [configSlice.name]: configSlice.reducer, - [wellknownApi.reducerPath]: wellknownApi.reducer, -}); +/** + * Root reducer built with combineSlices to support lazy injection. + * External slices (e.g. oidcApi) can be injected via rootReducer.inject(). + */ +export const rootReducer = combineSlices( + journeyApi, + configSlice, + wellknownApi, +).withLazyLoadedSlices(); + +export type RootState = ReturnType; + +/** + * Internal store shape carrying root reducer and dynamic middleware + * so that oidc-client can inject its reducers and middleware at init time. + */ +export interface InjectableStore { + readonly store: ReturnType>; + readonly rootReducer: typeof rootReducer; + readonly dynamicMiddleware: ReturnType; +} + +/** The inner Redux store type — used by effects that need dispatch/getState. */ +export type JourneyStore = InjectableStore['store']; export const createJourneyStore = ({ requestMiddleware, @@ -25,8 +44,10 @@ export const createJourneyStore = ({ }: { requestMiddleware?: RequestMiddleware[]; logger?: ReturnType; -}) => { - return configureStore({ +}): InjectableStore => { + const dynamicMiddleware = createDynamicMiddleware(); + + const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ @@ -39,8 +60,19 @@ export const createJourneyStore = ({ }, }) .concat(journeyApi.middleware) - .concat(wellknownApi.middleware), + .concat(wellknownApi.middleware) + .concat(dynamicMiddleware.middleware), }); + + return { store, rootReducer, dynamicMiddleware }; }; -export type RootState = ReturnType; +/** Cast InjectableStore to the opaque SdkStore for public API exposure. */ +export function toSdkStore(injectable: InjectableStore): object { + return injectable as unknown as object; +} + +/** Recover the InjectableStore from an opaque SdkStore handle. */ +export function fromSdkStore(sdkStore: object): InjectableStore { + return sdkStore as unknown as InjectableStore; +} diff --git a/packages/oidc-client/api-report/oidc-client.api.md b/packages/oidc-client/api-report/oidc-client.api.md index 02a90f352a..d9fe8e87bd 100644 --- a/packages/oidc-client/api-report/oidc-client.api.md +++ b/packages/oidc-client/api-report/oidc-client.api.md @@ -23,6 +23,7 @@ import { OidcConfig } from '@forgerock/sdk-types'; import { QueryDefinition } from '@reduxjs/toolkit/query'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; import { ResponseType as ResponseType_2 } from '@forgerock/sdk-types'; +import type { SdkStore } from '@forgerock/sdk-types'; import { StorageConfig } from '@forgerock/storage'; import { StoreEnhancer } from '@reduxjs/toolkit'; import { ThunkDispatch } from '@reduxjs/toolkit'; @@ -283,7 +284,7 @@ export function oidc(input: { custom?: CustomLogger; }; storage?: Partial; -}): Promise<{ +}, sharedStore?: SdkStore): Promise<{ error: string; type: string; subscribe?: undefined; diff --git a/packages/oidc-client/api-report/oidc-client.types.api.md b/packages/oidc-client/api-report/oidc-client.types.api.md index 02a90f352a..d9fe8e87bd 100644 --- a/packages/oidc-client/api-report/oidc-client.types.api.md +++ b/packages/oidc-client/api-report/oidc-client.types.api.md @@ -23,6 +23,7 @@ import { OidcConfig } from '@forgerock/sdk-types'; import { QueryDefinition } from '@reduxjs/toolkit/query'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; import { ResponseType as ResponseType_2 } from '@forgerock/sdk-types'; +import type { SdkStore } from '@forgerock/sdk-types'; import { StorageConfig } from '@forgerock/storage'; import { StoreEnhancer } from '@reduxjs/toolkit'; import { ThunkDispatch } from '@reduxjs/toolkit'; @@ -283,7 +284,7 @@ export function oidc(input: { custom?: CustomLogger; }; storage?: Partial; -}): Promise<{ +}, sharedStore?: SdkStore): Promise<{ error: string; type: string; subscribe?: undefined; diff --git a/packages/oidc-client/src/lib/client.store.ts b/packages/oidc-client/src/lib/client.store.ts index 15ec19bfaa..86b6b67eaa 100644 --- a/packages/oidc-client/src/lib/client.store.ts +++ b/packages/oidc-client/src/lib/client.store.ts @@ -12,7 +12,7 @@ import { causeIsDie, exitIsFail, exitIsSuccess } from 'effect/Micro'; import { authorizeµ, createParAuthorizeUrlµ } from './authorize.request.js'; import { buildTokenExchangeµ } from './exchange.request.js'; -import { createClientStore, createTokenError } from './client.store.utils.js'; +import { createClientStore, createTokenError, injectIntoStore } from './client.store.utils.js'; import { handleMicroExit } from '@forgerock/sdk-utilities'; import { isExpiryWithinThreshold } from './token.utils.js'; import { logoutµ } from './logout.request.js'; @@ -21,7 +21,7 @@ import { sessionCheckNoneµ, sessionCheckIdTokenµ } from './session.micros.js'; import { wellknownApi, wellknownSelector } from '@forgerock/sdk-wellknown'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; -import type { GenericError, GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; +import type { GenericError, GetAuthorizationUrlOptions, SdkStore } from '@forgerock/sdk-types'; import type { CustomLogger, LogLevel } from '@forgerock/sdk-logger'; import type { StorageConfig } from '@forgerock/storage'; @@ -51,20 +51,23 @@ import type { SessionCheckOptions, SessionCheckSuccess } from './session.types.j * @param {Partial} param.storage - optional storage configuration for persisting OIDC tokens. * @returns {ReturnType} - Returns an object with methods for authorization, token exchange, user info retrieval, and logout. */ -export async function oidc({ - config, - requestMiddleware, - logger, - storage, -}: { - config: OidcConfig; - requestMiddleware?: RequestMiddleware[]; - logger?: { - level: LogLevel; - custom?: CustomLogger; - }; - storage?: Partial; -}) { +export async function oidc( + { + config, + requestMiddleware, + logger, + storage, + }: { + config: OidcConfig; + requestMiddleware?: RequestMiddleware[]; + logger?: { + level: LogLevel; + custom?: CustomLogger; + }; + storage?: Partial; + }, + sharedStore?: SdkStore, +) { const log = loggerFn({ level: logger?.level ?? config.log ?? 'error', custom: logger?.custom, @@ -76,7 +79,9 @@ export async function oidc({ prefix: storage?.prefix || 'pic', ...storage, } as StorageConfig); - const store = createClientStore({ requestMiddleware, logger: log }); + const store = sharedStore + ? injectIntoStore(sharedStore) + : createClientStore({ requestMiddleware, logger: log }); if (!config?.serverConfig?.wellknown) { return { diff --git a/packages/oidc-client/src/lib/client.store.utils.ts b/packages/oidc-client/src/lib/client.store.utils.ts index c58d13b6ab..86be733f02 100644 --- a/packages/oidc-client/src/lib/client.store.utils.ts +++ b/packages/oidc-client/src/lib/client.store.utils.ts @@ -14,6 +14,37 @@ import { wellknownApi } from '@forgerock/sdk-wellknown'; import type { GenericError } from '@forgerock/sdk-types'; import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +/** + * Internal InjectableStore shape — mirrored from davinci/journey-client. + * Only used within oidc-client for the shared-store injection path. + */ +interface InjectableStore { + readonly store: ReturnType; + readonly rootReducer: { inject: (api: unknown) => void }; + readonly dynamicMiddleware: { addMiddleware: (...mw: unknown[]) => void }; +} + +/** + * Recovers the InjectableStore from an opaque SdkStore handle so oidc-client + * can inject its own reducer and middleware into a store owned by another client. + */ +function fromSdkStore(sdkStore: object): InjectableStore { + return sdkStore as unknown as InjectableStore; +} + +/** + * Lazily injects oidcApi reducer and middleware into a store that was created + * by davinci() or journey(). Safe to call multiple times — RTK deduplicates injections. + * The cast is safe: after injection, the store's state will contain the oidc and wellknown + * slices, matching the shape produced by createClientStore. + */ +export function injectIntoStore(sdkStore: object): ReturnType { + const { store, rootReducer, dynamicMiddleware } = fromSdkStore(sdkStore); + rootReducer.inject(oidcApi); + dynamicMiddleware.addMiddleware(oidcApi.middleware); + return store as unknown as ReturnType; +} + /** * @function createClientStore * @description Creates a Redux store configured with OIDC and well-known APIs. diff --git a/packages/oidc-client/src/lib/shared-store.test.ts b/packages/oidc-client/src/lib/shared-store.test.ts new file mode 100644 index 0000000000..1f7ed6a7c8 --- /dev/null +++ b/packages/oidc-client/src/lib/shared-store.test.ts @@ -0,0 +1,236 @@ +// @vitest-environment node +/* + * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { combineSlices, configureStore, createDynamicMiddleware } from '@reduxjs/toolkit'; + +import { wellknownApi } from '@forgerock/sdk-wellknown'; +import { oidc } from './client.store.js'; +import { injectIntoStore } from './client.store.utils.js'; +import { oidcApi } from './oidc.api.js'; + +import type { SdkStore } from '@forgerock/sdk-types'; +import type { OidcConfig } from './config.types.js'; + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +const TEST_WELLKNOWN_URL = + 'https://example.pingone.com/test-env/as/.well-known/openid-configuration'; + +const mockWellknownResponse = { + issuer: 'https://example.pingone.com/test-env/as', + authorization_endpoint: 'https://example.pingone.com/test-env/as/authorize', + token_endpoint: 'https://example.pingone.com/test-env/as/token', + userinfo_endpoint: 'https://example.pingone.com/test-env/as/userinfo', + jwks_uri: 'https://example.pingone.com/test-env/as/jwks', + revocation_endpoint: 'https://example.pingone.com/test-env/as/revoke', + introspection_endpoint: 'https://example.pingone.com/test-env/as/introspect', + pushed_authorization_request_endpoint: 'https://example.pingone.com/test-env/as/par', +}; + +const oidcConfig: OidcConfig = { + clientId: 'test-client-id', + redirectUri: 'http://localhost/callback', + scope: 'openid profile', + serverConfig: { wellknown: TEST_WELLKNOWN_URL }, + responseType: 'code', +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function makeStorageStub() { + const store = new Map(); + return { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => store.set(key, value), + removeItem: (key: string) => store.delete(key), + clear: () => store.clear(), + get length() { + return store.size; + }, + key: (i: number) => [...store.keys()][i] ?? null, + }; +} + +/** + * Constructs a minimal InjectableStore that mirrors what davinci()/journey() creates, + * but without importing those packages. Uses only RTK primitives. + */ +function makeSharedStore(): SdkStore { + const dynamicMiddleware = createDynamicMiddleware(); + const rootReducer = combineSlices(wellknownApi).withLazyLoadedSlices(); + const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(wellknownApi.middleware).concat(dynamicMiddleware.middleware), + }); + // Cast to SdkStore the same way toSdkStore() does + return { store, rootReducer, dynamicMiddleware } as unknown as SdkStore; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('injectIntoStore()', () => { + it('calls rootReducer.inject with oidcApi', () => { + const dynamicMiddleware = createDynamicMiddleware(); + const rootReducer = combineSlices(wellknownApi).withLazyLoadedSlices(); + const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(dynamicMiddleware.middleware), + }); + + const injectable = { store, rootReducer, dynamicMiddleware } as unknown as SdkStore; + + const injectSpy = vi.spyOn(rootReducer, 'inject'); + injectIntoStore(injectable); + + expect(injectSpy).toHaveBeenCalledWith(oidcApi); + }); + + it('calls dynamicMiddleware.addMiddleware with oidcApi.middleware', () => { + const dynamicMiddleware = createDynamicMiddleware(); + const rootReducer = combineSlices(wellknownApi).withLazyLoadedSlices(); + const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(dynamicMiddleware.middleware), + }); + + const injectable = { store, rootReducer, dynamicMiddleware } as unknown as SdkStore; + + const addMiddlewareSpy = vi.spyOn(dynamicMiddleware, 'addMiddleware'); + injectIntoStore(injectable); + + expect(addMiddlewareSpy).toHaveBeenCalledWith(oidcApi.middleware); + }); +}); + +describe('oidc() standalone — no shared store', () => { + let fetchCallCount = 0; + + beforeEach(() => { + fetchCallCount = 0; + vi.stubGlobal('localStorage', makeStorageStub()); + vi.stubGlobal('sessionStorage', makeStorageStub()); + + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const url = typeof input === 'string' ? input : (input as Request).url; + fetchCallCount++; + + if (url.includes('.well-known')) { + return new Response(JSON.stringify(mockWellknownResponse), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return new Response(JSON.stringify({}), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it('creates its own store and returns a working client', async () => { + const client = await oidc({ config: oidcConfig }); + + expect('error' in client).toBe(false); + if ('error' in client) throw new Error('Expected oidc client, got error'); + + expect(client.subscribe).toBeInstanceOf(Function); + expect(client.token).toBeDefined(); + expect(client.authorize).toBeDefined(); + }); + + it('fetches wellknown during init', async () => { + await oidc({ config: oidcConfig }); + + expect(fetchCallCount).toBeGreaterThan(0); + }); +}); + +describe('oidc() with shared store', () => { + let fetchCallCount = 0; + + beforeEach(() => { + fetchCallCount = 0; + vi.stubGlobal('localStorage', makeStorageStub()); + vi.stubGlobal('sessionStorage', makeStorageStub()); + + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const url = typeof input === 'string' ? input : (input as Request).url; + fetchCallCount++; + + if (url.includes('.well-known')) { + return new Response(JSON.stringify(mockWellknownResponse), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return new Response(JSON.stringify({}), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it('resolves without error when given a shared SdkStore', async () => { + const sharedStore = makeSharedStore(); + const client = await oidc({ config: oidcConfig }, sharedStore); + + expect('error' in client).toBe(false); + if ('error' in client) throw new Error('Expected oidc client, got error'); + + expect(client.subscribe).toBeInstanceOf(Function); + expect(client.token).toBeDefined(); + expect(client.authorize).toBeDefined(); + }); + + it('subscribe() returns an unsubscribe function', async () => { + const sharedStore = makeSharedStore(); + const client = await oidc({ config: oidcConfig }, sharedStore); + + if ('error' in client) throw new Error('Expected oidc client, got error'); + + const listener = vi.fn(); + const unsubscribe = client.subscribe(listener); + + expect(unsubscribe).toBeInstanceOf(Function); + }); + + it('reuses cached wellknown response — no additional fetch when store already has it', async () => { + const sharedStore = makeSharedStore(); + + // Pre-populate the wellknown cache by routing through the typed store from injectIntoStore + const typedStore = injectIntoStore(sharedStore); + await typedStore.dispatch(wellknownApi.endpoints.configuration.initiate(TEST_WELLKNOWN_URL)); + const fetchesAfterPreload = fetchCallCount; + + // oidc() should hit the cache, not make another wellknown request + await oidc({ config: oidcConfig }, sharedStore); + + expect(fetchCallCount).toBe(fetchesAfterPreload); + }); +}); diff --git a/packages/sdk-effects/wellknown/vite.config.ts b/packages/sdk-effects/wellknown/vite.config.ts index dd352b8bbd..cef160f8ba 100644 --- a/packages/sdk-effects/wellknown/vite.config.ts +++ b/packages/sdk-effects/wellknown/vite.config.ts @@ -7,6 +7,7 @@ export default defineConfig(() => ({ test: { watch: false, globals: true, + passWithNoTests: true, environment: 'node', include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], diff --git a/packages/sdk-types/src/index.ts b/packages/sdk-types/src/index.ts index 7d5ffabb36..10b862df1d 100644 --- a/packages/sdk-types/src/index.ts +++ b/packages/sdk-types/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -15,3 +15,4 @@ export * from './lib/tokens.types.js'; export * from './lib/config.types.js'; export * from './lib/authorize.types.js'; export * from './lib/policy.types.js'; +export * from './lib/store.types.js'; diff --git a/packages/sdk-types/src/lib/store.types.ts b/packages/sdk-types/src/lib/store.types.ts new file mode 100644 index 0000000000..6653db7c68 --- /dev/null +++ b/packages/sdk-types/src/lib/store.types.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Opaque handle to a shared SDK Redux store. + * + * Consumers can pass this between SDK client factories but cannot access + * Redux internals (dispatch, getState, etc.) through this type — by design. + */ +export interface SdkStore { + readonly __sdkStoreBrand: symbol; +} From db906f5a64a79112a0a2f9f08567bb387d9e7b3b Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Mon, 27 Jul 2026 15:38:33 -0600 Subject: [PATCH 3/4] fix(oidc): warn when requestMiddleware is silently dropped with sharedStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit requestMiddleware has no effect when oidc() uses an injected store — the owning store's thunk.extraArgument is used instead. Emit an explicit warn so callers aren't left debugging silent behavior. Test coverage: adds idempotency, behavioral subscribe, cache-reuse, requestMiddleware-warning, and state-after-injection assertions. --- packages/oidc-client/src/lib/client.store.ts | 6 + .../oidc-client/src/lib/shared-store.test.ts | 137 ++++++++++++------ 2 files changed, 101 insertions(+), 42 deletions(-) diff --git a/packages/oidc-client/src/lib/client.store.ts b/packages/oidc-client/src/lib/client.store.ts index 86b6b67eaa..dfc5624580 100644 --- a/packages/oidc-client/src/lib/client.store.ts +++ b/packages/oidc-client/src/lib/client.store.ts @@ -79,6 +79,12 @@ export async function oidc( prefix: storage?.prefix || 'pic', ...storage, } as StorageConfig); + if (sharedStore && requestMiddleware?.length) { + log.warn( + '`requestMiddleware` is ignored when a `sharedStore` is provided. ' + + 'Pass request middleware to the davinci() or journey() factory that owns the store.', + ); + } const store = sharedStore ? injectIntoStore(sharedStore) : createClientStore({ requestMiddleware, logger: log }); diff --git a/packages/oidc-client/src/lib/shared-store.test.ts b/packages/oidc-client/src/lib/shared-store.test.ts index 1f7ed6a7c8..7b8b603f74 100644 --- a/packages/oidc-client/src/lib/shared-store.test.ts +++ b/packages/oidc-client/src/lib/shared-store.test.ts @@ -72,10 +72,28 @@ function makeSharedStore(): SdkStore { middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(wellknownApi.middleware).concat(dynamicMiddleware.middleware), }); - // Cast to SdkStore the same way toSdkStore() does return { store, rootReducer, dynamicMiddleware } as unknown as SdkStore; } +function makeFetchMock(onCall?: (url: string) => void) { + return vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const url = typeof input === 'string' ? input : (input as Request).url; + onCall?.(url); + + if (url.includes('.well-known')) { + return new Response(JSON.stringify(mockWellknownResponse), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return new Response(JSON.stringify({}), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }); +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -114,6 +132,32 @@ describe('injectIntoStore()', () => { expect(addMiddlewareSpy).toHaveBeenCalledWith(oidcApi.middleware); }); + + it('oidcApi slice is readable from store state after injection', () => { + const sharedStore = makeSharedStore(); + const typedStore = injectIntoStore(sharedStore); + + // combineSlices registers the reducer on inject(), but state is only recomputed + // on the next dispatch. Trigger a no-op action to force state recalculation. + typedStore.dispatch({ type: '@@test/init' }); + const state = typedStore.getState() as Record; + + expect(state).toHaveProperty(oidcApi.reducerPath); + }); + + it('is idempotent — calling twice does not throw or corrupt state', () => { + const sharedStore = makeSharedStore(); + + expect(() => { + injectIntoStore(sharedStore); + injectIntoStore(sharedStore); + }).not.toThrow(); + + const typedStore = injectIntoStore(sharedStore); + typedStore.dispatch({ type: '@@test/init' }); + const state = typedStore.getState() as Record; + expect(state).toHaveProperty(oidcApi.reducerPath); + }); }); describe('oidc() standalone — no shared store', () => { @@ -123,22 +167,8 @@ describe('oidc() standalone — no shared store', () => { fetchCallCount = 0; vi.stubGlobal('localStorage', makeStorageStub()); vi.stubGlobal('sessionStorage', makeStorageStub()); - - vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { - const url = typeof input === 'string' ? input : (input as Request).url; - fetchCallCount++; - - if (url.includes('.well-known')) { - return new Response(JSON.stringify(mockWellknownResponse), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - } - - return new Response(JSON.stringify({}), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); + makeFetchMock((url) => { + if (url.includes('.well-known')) fetchCallCount++; }); }); @@ -166,28 +196,14 @@ describe('oidc() standalone — no shared store', () => { }); describe('oidc() with shared store', () => { - let fetchCallCount = 0; + let wellknownFetchCount = 0; beforeEach(() => { - fetchCallCount = 0; + wellknownFetchCount = 0; vi.stubGlobal('localStorage', makeStorageStub()); vi.stubGlobal('sessionStorage', makeStorageStub()); - - vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { - const url = typeof input === 'string' ? input : (input as Request).url; - fetchCallCount++; - - if (url.includes('.well-known')) { - return new Response(JSON.stringify(mockWellknownResponse), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - } - - return new Response(JSON.stringify({}), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); + makeFetchMock((url) => { + if (url.includes('.well-known')) wellknownFetchCount++; }); }); @@ -208,29 +224,66 @@ describe('oidc() with shared store', () => { expect(client.authorize).toBeDefined(); }); - it('subscribe() returns an unsubscribe function', async () => { + it('subscribe() fires when oidcApi state changes on the shared store', async () => { const sharedStore = makeSharedStore(); const client = await oidc({ config: oidcConfig }, sharedStore); if ('error' in client) throw new Error('Expected oidc client, got error'); const listener = vi.fn(); - const unsubscribe = client.subscribe(listener); + client.subscribe(listener); - expect(unsubscribe).toBeInstanceOf(Function); + // Dispatch an oidcApi mutation — any action that causes a state change will notify the subscriber + const typedStore = injectIntoStore(sharedStore); + typedStore.dispatch({ type: 'test/action' }); + + expect(listener).toHaveBeenCalled(); }); it('reuses cached wellknown response — no additional fetch when store already has it', async () => { const sharedStore = makeSharedStore(); - // Pre-populate the wellknown cache by routing through the typed store from injectIntoStore + // Simulate the owning store (davinci/journey) having already fetched wellknown const typedStore = injectIntoStore(sharedStore); await typedStore.dispatch(wellknownApi.endpoints.configuration.initiate(TEST_WELLKNOWN_URL)); - const fetchesAfterPreload = fetchCallCount; + const fetchesAfterOwnerInit = wellknownFetchCount; - // oidc() should hit the cache, not make another wellknown request + // oidc() using the same wellknown URL should hit the RTK Query cache await oidc({ config: oidcConfig }, sharedStore); - expect(fetchCallCount).toBe(fetchesAfterPreload); + expect(wellknownFetchCount).toBe(fetchesAfterOwnerInit); + }); + + it('warns when requestMiddleware is passed alongside sharedStore', async () => { + const sharedStore = makeSharedStore(); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(vi.fn()); + + // Set log level to 'warn' so the logger actually emits the warning to console.warn. + // The default level is 'error', which silently suppresses warn/info/debug messages. + await oidc( + { + config: oidcConfig, + logger: { level: 'warn' }, + requestMiddleware: [() => () => (action: unknown) => action], + }, + sharedStore, + ); + + const warnCalls = warnSpy.mock.calls.map((args) => args.join(' ')); + expect(warnCalls.some((msg) => msg.includes('requestMiddleware'))).toBe(true); + + warnSpy.mockRestore(); + }); + + it('does not warn when no requestMiddleware is passed with sharedStore', async () => { + const sharedStore = makeSharedStore(); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(vi.fn()); + + await oidc({ config: oidcConfig, logger: { level: 'warn' } }, sharedStore); + + const warnCalls = warnSpy.mock.calls.map((args) => args.join(' ')); + expect(warnCalls.some((msg) => msg.includes('requestMiddleware'))).toBe(false); + + warnSpy.mockRestore(); }); }); From 504562dd437f680bb5b81d2e5240e98a138981ed Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Wed, 5 Aug 2026 15:36:38 -0600 Subject: [PATCH 4/4] =?UTF-8?q?build:=20migrate=20effect=20v3=20=E2=86=92?= =?UTF-8?q?=20v4=20(beta.103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate all packages from effect@^3.20 to effect@4.0.0-beta.103. Key changes: - Replace Micro with Effect throughout oidc-client and davinci-client - Migrate Either to Result in sdk-utilities and journey-client - Migrate MicroExit/exitIsFail/exitIsSuccess to Exit/Cause API - Update e2e/mock-api-v2 from @effect/platform to effect/unstable/httpapi - Bump vitest catalog to ^4.1.0 to match @effect/vitest peer requirement - Remove consolidated packages: @effect/platform, @effect/cli - Fix handleMicroExit → handleExit (renamed) - Add vi.clearAllMocks() to oidc-client afterEach (accumulated mock counts) --- e2e/mock-api-v2/package.json | 3 +- .../src/handlers/authorize.handler.ts | 5 +- .../src/handlers/capabilities.handler.ts | 15 +- .../src/handlers/end-session.handler.ts | 3 +- .../src/handlers/healthcheck.handler.ts | 2 +- .../handlers/open-id-configuration.handler.ts | 6 +- .../src/handlers/revoke.handler.ts | 2 +- e2e/mock-api-v2/src/handlers/token.handler.ts | 2 +- .../src/handlers/userinfo.handler.ts | 2 +- e2e/mock-api-v2/src/helpers/match.ts | 12 +- e2e/mock-api-v2/src/main.ts | 82 +- .../src/middleware/Authorization.ts | 33 +- .../src/middleware/CookieMiddleware.ts | 95 +- e2e/mock-api-v2/src/middleware/Session.ts | 58 +- .../src/schemas/authorize.schema.ts | 3 +- .../capabilities.request.schema.ts | 2 +- .../capabilities.response.schema.ts | 2 +- .../open-id-configuration-response.schema.ts | 6 +- ...return-success-response-redirect.schema.ts | 3 +- .../src/schemas/revoke/revoke.schema.ts | 2 +- .../src/schemas/token/token.schema.ts | 6 +- .../src/services/mock-env-helpers/index.ts | 37 +- .../src/services/session.service.ts | 176 +-- .../src/services/tokens.service.ts | 49 +- .../src/services/userinfo.service.ts | 29 +- e2e/mock-api-v2/src/spec.ts | 132 +- package.json | 3 +- .../api-report/davinci-client.api.md | 26 +- .../api-report/davinci-client.types.api.md | 26 +- .../src/lib/client.store.effects.test.ts | 20 +- .../src/lib/client.store.effects.ts | 104 +- .../davinci-client/src/lib/client.store.ts | 22 +- packages/davinci-client/src/lib/fido/fido.ts | 67 +- .../src/lib/password-policy.rules.ts | 6 +- .../journey-client/src/lib/client.store.ts | 14 +- .../src/lib/journey.utils.test.ts | 40 +- .../journey-client/src/lib/journey.utils.ts | 14 +- .../src/lib/authorize.request.micros.test.ts | 184 +-- .../src/lib/authorize.request.micros.ts | 70 +- .../oidc-client/src/lib/authorize.request.ts | 48 +- .../src/lib/authorize.request.utils.test.ts | 84 +- packages/oidc-client/src/lib/client.store.ts | 192 +-- .../oidc-client/src/lib/exchange.request.ts | 26 +- .../src/lib/exchange.utils.test.ts | 73 +- .../oidc-client/src/lib/exchange.utils.ts | 16 +- .../src/lib/logout.request.test.ts | 158 +-- .../oidc-client/src/lib/logout.request.ts | 22 +- .../src/lib/session.micros.test.ts | 336 +++-- .../oidc-client/src/lib/session.micros.ts | 66 +- .../src/lib/config/config.effects.ts | 16 +- .../src/lib/config/config.test.ts | 140 +- .../src/lib/config/config.types.ts | 8 +- .../src/lib/config/config.utils.ts | 119 +- packages/sdk-utilities/src/lib/micro.utils.ts | 26 +- pnpm-lock.yaml | 1173 ++++++----------- pnpm-workspace.yaml | 19 +- tools/release/package.json | 1 - tools/user-scripts/package.json | 1 - 58 files changed, 1828 insertions(+), 2059 deletions(-) diff --git a/e2e/mock-api-v2/package.json b/e2e/mock-api-v2/package.json index 3f29b0f9ed..436aeb92a9 100644 --- a/e2e/mock-api-v2/package.json +++ b/e2e/mock-api-v2/package.json @@ -6,7 +6,7 @@ "type": "module", "main": "./src/main.js", "scripts": { - "build": "pnpm nx nxBuild", + "build": "pnpm nx build", "dev": "node dist/src/main.js --watch-path=./", "lint": "pnpm nx nxLint", "serve": "node dist/src/main.js", @@ -15,7 +15,6 @@ "dependencies": { "@effect/language-service": "catalog:effect", "@effect/opentelemetry": "catalog:effect", - "@effect/platform": "catalog:effect", "@effect/platform-node": "catalog:effect", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", diff --git a/e2e/mock-api-v2/src/handlers/authorize.handler.ts b/e2e/mock-api-v2/src/handlers/authorize.handler.ts index c636c9e762..10572db05e 100644 --- a/e2e/mock-api-v2/src/handlers/authorize.handler.ts +++ b/e2e/mock-api-v2/src/handlers/authorize.handler.ts @@ -6,11 +6,12 @@ */ import { Effect, pipe } from 'effect'; import { MockApi } from '../spec.js'; -import { HttpApiBuilder, HttpApiError, HttpServerResponse } from '@effect/platform'; +import { HttpApiBuilder, HttpApiError } from 'effect/unstable/httpapi'; +import * as HttpServerResponse from 'effect/unstable/http/HttpServerResponse'; import { getFirstElementAndRespond } from '../services/mock-env-helpers/index.js'; const AuthorizeHandlerMock = HttpApiBuilder.group(MockApi, 'Authorization', (handlers) => - handlers.handle('authorize', ({ urlParams }) => + handlers.handle('authorize', ({ query: urlParams }) => Effect.gen(function* () { const acr_value = urlParams?.acr_values ?? ''; diff --git a/e2e/mock-api-v2/src/handlers/capabilities.handler.ts b/e2e/mock-api-v2/src/handlers/capabilities.handler.ts index d82c62f3a5..2e99bd6009 100644 --- a/e2e/mock-api-v2/src/handlers/capabilities.handler.ts +++ b/e2e/mock-api-v2/src/handlers/capabilities.handler.ts @@ -6,12 +6,9 @@ */ import { Effect, pipe } from 'effect'; import { MockApi } from '../spec.js'; -import { - HttpApiBuilder, - HttpApiError, - HttpServerRequest, - HttpServerResponse, -} from '@effect/platform'; +import { HttpApiBuilder, HttpApiError } from 'effect/unstable/httpapi'; +import * as HttpServerRequest from 'effect/unstable/http/HttpServerRequest'; +import * as HttpServerResponse from 'effect/unstable/http/HttpServerResponse'; import { responseMap } from '../responses/index.js'; import { validator } from '../helpers/match.js'; import { returnSuccessResponseRedirect } from '../responses/return-success-redirect.js'; @@ -105,9 +102,9 @@ const CapabilitiesHandlerMock = HttpApiBuilder.group(MockApi, 'Capabilities', (h }, ), ), - Effect.flatMap((res) => HttpServerResponse.removeCookie(res, 'stepIndex')), - Effect.flatMap((res) => HttpServerResponse.setStatus(res, 200)), - Effect.flatMap((res) => + Effect.map((res) => HttpServerResponse.removeCookie(res, 'stepIndex')), + Effect.map((res) => HttpServerResponse.setStatus(res, 200)), + Effect.map((res) => HttpServerResponse.setHeader(res, 'Content-Type', 'application/json'), ), Effect.catchTag('CookieError', () => Effect.fail(new HttpApiError.InternalServerError())), diff --git a/e2e/mock-api-v2/src/handlers/end-session.handler.ts b/e2e/mock-api-v2/src/handlers/end-session.handler.ts index c25219e38b..1ca95a8139 100644 --- a/e2e/mock-api-v2/src/handlers/end-session.handler.ts +++ b/e2e/mock-api-v2/src/handlers/end-session.handler.ts @@ -5,7 +5,8 @@ * of the MIT license. See the LICENSE file for details. */ import { Effect, Console } from 'effect'; -import { HttpApiBuilder, HttpServerRequest } from '@effect/platform'; +import { HttpApiBuilder } from 'effect/unstable/httpapi'; +import * as HttpServerRequest from 'effect/unstable/http/HttpServerRequest'; import { MockApi } from '../spec.js'; import { SessionStorage } from '../services/session.service.js'; diff --git a/e2e/mock-api-v2/src/handlers/healthcheck.handler.ts b/e2e/mock-api-v2/src/handlers/healthcheck.handler.ts index fd9a27ce26..e40d5df7fb 100644 --- a/e2e/mock-api-v2/src/handlers/healthcheck.handler.ts +++ b/e2e/mock-api-v2/src/handlers/healthcheck.handler.ts @@ -1,4 +1,4 @@ -import { HttpApiBuilder } from '@effect/platform'; +import { HttpApiBuilder } from 'effect/unstable/httpapi'; import { MockApi } from '../spec.js'; import { Effect } from 'effect'; diff --git a/e2e/mock-api-v2/src/handlers/open-id-configuration.handler.ts b/e2e/mock-api-v2/src/handlers/open-id-configuration.handler.ts index f6b4d2a76e..3b5076e43d 100644 --- a/e2e/mock-api-v2/src/handlers/open-id-configuration.handler.ts +++ b/e2e/mock-api-v2/src/handlers/open-id-configuration.handler.ts @@ -6,11 +6,11 @@ */ import { Effect } from 'effect'; import { MockApi } from '../spec.js'; -import { HttpApiBuilder } from '@effect/platform'; -import { HttpServerRequest } from '@effect/platform/HttpServerRequest'; +import { HttpApiBuilder } from 'effect/unstable/httpapi'; +import { HttpServerRequest } from 'effect/unstable/http/HttpServerRequest'; const OpenidConfigMock = HttpApiBuilder.group(MockApi, 'OpenIDConfig', (handlers) => - handlers.handle('openid', ({ path: { envid } }) => + handlers.handle('openid', ({ params: { envid } }) => Effect.gen(function* () { const request = yield* HttpServerRequest; const url = new URL(request.url); diff --git a/e2e/mock-api-v2/src/handlers/revoke.handler.ts b/e2e/mock-api-v2/src/handlers/revoke.handler.ts index b25c8da876..05b0eb43d4 100644 --- a/e2e/mock-api-v2/src/handlers/revoke.handler.ts +++ b/e2e/mock-api-v2/src/handlers/revoke.handler.ts @@ -6,7 +6,7 @@ */ import { MockApi } from '../spec.js'; import { Tokens } from '../services/tokens.service.js'; -import { HttpApiBuilder } from '@effect/platform'; +import { HttpApiBuilder } from 'effect/unstable/httpapi'; import { Effect } from 'effect'; const RevokeTokenHandler = HttpApiBuilder.group(MockApi, 'Revoke', (handlers) => diff --git a/e2e/mock-api-v2/src/handlers/token.handler.ts b/e2e/mock-api-v2/src/handlers/token.handler.ts index 5eefdd073d..a0a1007f70 100644 --- a/e2e/mock-api-v2/src/handlers/token.handler.ts +++ b/e2e/mock-api-v2/src/handlers/token.handler.ts @@ -6,7 +6,7 @@ */ import { MockApi } from '../spec.js'; import { Tokens } from '../services/tokens.service.js'; -import { HttpApiBuilder } from '@effect/platform'; +import { HttpApiBuilder } from 'effect/unstable/httpapi'; import { Effect } from 'effect'; const TokensHandler = HttpApiBuilder.group(MockApi, 'Tokens', (handlers) => diff --git a/e2e/mock-api-v2/src/handlers/userinfo.handler.ts b/e2e/mock-api-v2/src/handlers/userinfo.handler.ts index bea15715eb..b1862f7028 100644 --- a/e2e/mock-api-v2/src/handlers/userinfo.handler.ts +++ b/e2e/mock-api-v2/src/handlers/userinfo.handler.ts @@ -7,7 +7,7 @@ import { Effect } from 'effect'; import { MockApi } from '../spec.js'; import { UserInfo } from '../services/userinfo.service.js'; -import { HttpApiBuilder, HttpApiError } from '@effect/platform'; +import { HttpApiBuilder, HttpApiError } from 'effect/unstable/httpapi'; import { BearerToken } from '../middleware/Authorization.js'; const UserInfoMockHandler = HttpApiBuilder.group(MockApi, 'ProtectedRequests', (handlers) => diff --git a/e2e/mock-api-v2/src/helpers/match.ts b/e2e/mock-api-v2/src/helpers/match.ts index 511f2ffda2..e9ca41cac1 100644 --- a/e2e/mock-api-v2/src/helpers/match.ts +++ b/e2e/mock-api-v2/src/helpers/match.ts @@ -6,7 +6,7 @@ */ import { Effect, Match, Schema } from 'effect'; -import { HttpApiError } from '@effect/platform'; +import { HttpApiError } from 'effect/unstable/httpapi'; import { CapabilitiesRequestBody } from '../schemas/capabilities/capabilities.request.schema.js'; type PingRequestData = Schema.Schema.Type; @@ -21,13 +21,11 @@ const validator = Match.type().pipe( Match.when( { parameters: { data: { formData: { username: Match.string, password: Match.string } } } }, ({ parameters }) => - Effect.if( + Effect.suspend(() => parameters.data.formData.username == 'testuser' && - parameters.data.formData.password === 'Password', - { - onFalse: () => Effect.fail(new HttpApiError.Unauthorized()), - onTrue: () => Effect.succeed(true), - }, + parameters.data.formData.password === 'Password' + ? Effect.succeed(true) + : Effect.fail(new HttpApiError.Unauthorized()), ), ), Match.orElse(() => Effect.succeed(true)), diff --git a/e2e/mock-api-v2/src/main.ts b/e2e/mock-api-v2/src/main.ts index 92e2acf3ee..259e0c7b7d 100644 --- a/e2e/mock-api-v2/src/main.ts +++ b/e2e/mock-api-v2/src/main.ts @@ -4,10 +4,14 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { Layer } from 'effect'; +import { Effect, Layer } from 'effect'; import { NodeHttpServer, NodeRuntime } from '@effect/platform-node'; import { MockApi } from './spec.js'; -import { HttpApiBuilder, HttpApiSwagger, HttpMiddleware, HttpServer } from '@effect/platform'; +import { HttpApiBuilder, HttpApiSwagger } from 'effect/unstable/httpapi'; +import * as HttpMiddleware from 'effect/unstable/http/HttpMiddleware'; +import * as HttpRouter from 'effect/unstable/http/HttpRouter'; +import * as HttpServer from 'effect/unstable/http/HttpServer'; +import type { ServeError } from 'effect/unstable/http/HttpServerError'; import { createServer } from 'node:http'; import { HealthCheckLive } from './handlers/healthcheck.handler.js'; import { OpenidConfigMock } from './handlers/open-id-configuration.handler.js'; @@ -26,47 +30,63 @@ import { BatchSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trac import { EndSessionHandlerMock } from './handlers/end-session.handler.js'; import { RevokeTokenHandler } from './handlers/revoke.handler.js'; -const Services = [ - Layer.provide(TokensMock), - Layer.provide(IncrementStepIndexMock), - Layer.provide(AuthorizationMock), - Layer.provide(UserInfoMockService), - Layer.provide(SessionMiddlewareMock), - Layer.provide(SessionStorage.Default), -] as const; - const NodeSdkLive = NodeSdk.layer(() => ({ resource: { serviceName: 'Mock-Api' }, spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()), })); -const APIMock = HttpApiBuilder.api(MockApi).pipe( - Layer.provide(HealthCheckLive), - Layer.provide(OpenidConfigMock), - Layer.provide(AuthorizeHandlerMock), - Layer.provide(TokensHandler), - Layer.provide(CapabilitiesHandlerMock), - Layer.provide(UserInfoMockHandler), - Layer.provide(EndSessionHandlerMock), - Layer.provide(RevokeTokenHandler), - ...Services, +// Wire SessionStorage into SessionMiddlewareMock +const SessionLayer = Layer.provide( + SessionMiddlewareMock, + Layer.effect(SessionStorage, SessionStorage.make), +); + +// Merge all group handlers +const HandlersLayer = Layer.mergeAll( + HealthCheckLive, + OpenidConfigMock, + AuthorizeHandlerMock, + TokensHandler, + CapabilitiesHandlerMock, + UserInfoMockHandler, + EndSessionHandlerMock, + RevokeTokenHandler, +); + +// Merge all services +const ServicesLayer = Layer.mergeAll( + TokensMock, + IncrementStepIndexMock, + AuthorizationMock, + UserInfoMockService, + SessionLayer, +); + +// Build application routes layer with all handlers and services provided in one step each +const AppLayer = HttpApiBuilder.layer(MockApi).pipe( + Layer.provide(HandlersLayer), + Layer.provide(ServicesLayer), +); + +// Compose app + swagger, then provide the router service +const AppWithSwagger = Layer.merge(AppLayer, HttpApiSwagger.layer(MockApi)).pipe( + Layer.provide(HttpRouter.layer), ); -const ServerMock = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( - Layer.provide(HttpApiSwagger.layer()), - Layer.provide( - HttpApiBuilder.middlewareCors({ +const ServerMock = HttpRouter.serve(AppWithSwagger, { + middleware: (app) => + HttpMiddleware.cors({ allowedMethods: ['GET', 'PUT', 'POST', 'OPTIONS'], allowedOrigins: ['*'], credentials: true, maxAge: 3600, - }), - ), - Layer.provide(APIMock), - - Layer.provide(NodeSdkLive), + })(HttpMiddleware.logger(app)), +}).pipe( HttpServer.withLogAddress, + Layer.provide(NodeSdkLive), Layer.provide(NodeHttpServer.layer(createServer, { port: 9443, host: 'localhost' })), ); -Layer.launch(ServerMock).pipe(NodeRuntime.runMain); +// TypeScript cannot fully resolve complex Effect layer generic compositions; +// all requirements ARE satisfied at runtime — NodeHttpServer provides FileSystem, Path, HttpPlatform, Etag. +NodeRuntime.runMain(Layer.launch(ServerMock) as Effect.Effect); diff --git a/e2e/mock-api-v2/src/middleware/Authorization.ts b/e2e/mock-api-v2/src/middleware/Authorization.ts index 1cfe3d8bee..d1873e8cb1 100644 --- a/e2e/mock-api-v2/src/middleware/Authorization.ts +++ b/e2e/mock-api-v2/src/middleware/Authorization.ts @@ -1,16 +1,18 @@ -import { Unauthorized } from '@effect/platform/HttpApiError'; -import { HttpApiMiddleware, HttpApiSecurity, OpenApi } from '@effect/platform'; -import { Brand, Context, Effect, Layer, Redacted } from 'effect'; +import { HttpApiError, HttpApiMiddleware, HttpApiSecurity, OpenApi } from 'effect/unstable/httpapi'; +import type { HttpServerResponse } from 'effect/unstable/http/HttpServerResponse'; +import { Brand, Context, Effect, Layer, Redacted, Types } from 'effect'; type BearerTokenValue = string & Brand.Brand<'BearerToken'>; const BearerTokenValue = Brand.nominal(); // Define a service that holds the bearer token -class BearerToken extends Context.Tag('BearerToken')() {} +class BearerToken extends Context.Service()('BearerToken') {} -class Authorization extends HttpApiMiddleware.Tag()('Authorization', { - failure: Unauthorized, - provides: BearerToken, +class Authorization extends HttpApiMiddleware.Service< + Authorization, + { provides: typeof BearerToken } +>()('Authorization', { + error: HttpApiError.Unauthorized, security: { myBearer: HttpApiSecurity.bearer.pipe( HttpApiSecurity.annotate(OpenApi.Description, 'Bearer token for API authentication'), @@ -24,20 +26,27 @@ const AuthorizationMock = Layer.effect( yield* Effect.log('creating Authorization middleware'); return { - myBearer: (bearerToken) => + myBearer: ( + httpEffect: Effect.Effect, + { + credential, + }: { credential: Redacted.Redacted; endpoint: unknown; group: unknown }, + ) => Effect.gen(function* () { - const tokenValue = Redacted.value(bearerToken); + const tokenValue = Redacted.value(credential); yield* Effect.log('checking bearer token', tokenValue); // Validation logic // 1. Check if token is empty // 2. Check if token has been revoked (has REVOKED_ prefix) if (!tokenValue || tokenValue.trim() === '' || tokenValue.startsWith('REVOKED_')) { - return yield* Effect.fail(new Unauthorized()); + return yield* Effect.fail(new HttpApiError.Unauthorized()); } - // Return the token value so routes can access it - return BearerTokenValue(tokenValue); + // Provide BearerToken and run the original effect + return yield* httpEffect.pipe( + Effect.provideService(BearerToken, BearerTokenValue(tokenValue)), + ); }), }; }), diff --git a/e2e/mock-api-v2/src/middleware/CookieMiddleware.ts b/e2e/mock-api-v2/src/middleware/CookieMiddleware.ts index 221709d57e..579762a5c2 100644 --- a/e2e/mock-api-v2/src/middleware/CookieMiddleware.ts +++ b/e2e/mock-api-v2/src/middleware/CookieMiddleware.ts @@ -2,17 +2,14 @@ * Copyright (c) 2025 Ping Identity Corporation. * MIT License */ -import { - HttpApiMiddleware, - HttpApp, - HttpServerRequest, - HttpServerResponse, -} from '@effect/platform'; -import { ResponseError } from '@effect/platform/HttpServerError'; +import { HttpApiMiddleware } from 'effect/unstable/httpapi'; +import * as HttpServerRequest from 'effect/unstable/http/HttpServerRequest'; +import * as HttpServerResponse from 'effect/unstable/http/HttpServerResponse'; import { Console, Effect, Layer } from 'effect'; +import type { HttpServerResponse as HttpServerResponseType } from 'effect/unstable/http/HttpServerResponse'; // Export the tag so you can .middleware(IncrementStepIndex) in your spec if desired -export class IncrementStepIndex extends HttpApiMiddleware.Tag()( +export class IncrementStepIndex extends HttpApiMiddleware.Service()( 'IncrementStepIndex', ) {} @@ -21,60 +18,48 @@ export const IncrementStepIndexMock = Layer.effect( Effect.gen(function* () { yield* Console.log('IncrementStepIndex: init'); - return Effect.gen(function* () { - // Read cookies from the current request - const request = yield* HttpServerRequest.HttpServerRequest; + return (httpEffect: Effect.Effect) => + Effect.gen(function* () { + // Read cookies from the current request + const request = yield* HttpServerRequest.HttpServerRequest; - // Parse existing stepIndex cookie or default to 0 - const cookies = request.cookies; - const currentStepIndex = cookies.stepIndex ? parseInt(cookies.stepIndex, 10) : 0; + // Parse existing stepIndex cookie or default to 0 + const cookies = request.cookies; + const currentStepIndex = cookies.stepIndex ? parseInt(cookies.stepIndex, 10) : 0; - // Normalize URL (strip query) and detect special flows - const urlPath = request.url.split('?')[0] ?? ''; - const isEndSessionRequest = - urlPath.includes('/endSession') || urlPath.includes('/end_session'); - const isAuthFlowRequest = urlPath.includes('/authorize') || urlPath.includes('/authenticate'); + // Normalize URL (strip query) and detect special flows + const urlPath = request.url.split('?')[0] ?? ''; + const isEndSessionRequest = + urlPath.includes('/endSession') || urlPath.includes('/end_session'); + const isAuthFlowRequest = + urlPath.includes('/authorize') || urlPath.includes('/authenticate'); - // Decide next value - let newStepIndex = currentStepIndex; - if (isEndSessionRequest) { - newStepIndex = 0; - yield* Console.log( - `IncrementStepIndex: end-session detected → resetting stepIndex to ${newStepIndex}`, - ); - } else if (isAuthFlowRequest) { - newStepIndex = currentStepIndex + 1; - yield* Console.log( - `IncrementStepIndex: auth flow → ${currentStepIndex} -> ${newStepIndex}`, - ); - } else { - yield* Console.log( - `IncrementStepIndex: other route ${urlPath} → keeping stepIndex ${currentStepIndex}`, - ); - } + // Decide next value + let newStepIndex = currentStepIndex; + if (isEndSessionRequest) { + newStepIndex = 0; + yield* Console.log( + `IncrementStepIndex: end-session detected → resetting stepIndex to ${newStepIndex}`, + ); + } else if (isAuthFlowRequest) { + newStepIndex = currentStepIndex + 1; + yield* Console.log( + `IncrementStepIndex: auth flow → ${currentStepIndex} -> ${newStepIndex}`, + ); + } else { + yield* Console.log( + `IncrementStepIndex: other route ${urlPath} → keeping stepIndex ${currentStepIndex}`, + ); + } - // Write cookie just before the response is sent - yield* HttpApp.appendPreResponseHandler((req, res) => - HttpServerResponse.setCookie(res, 'stepIndex', String(newStepIndex), { - // NOTE: mock defaults; tighten in prod (httpOnly: true, secure: true, sameSite: 'lax') + // Run the original handler then set the step index cookie on the response + const response = yield* httpEffect; + return yield* HttpServerResponse.setCookie(response, 'stepIndex', String(newStepIndex), { httpOnly: false, secure: false, sameSite: 'strict', path: '/', - }).pipe( - // If cookie setting fails, convert to a typed ResponseError for consistent diagnostics - Effect.catchTag( - 'CookieError', - () => - new ResponseError({ - request: req, - response: res, - reason: 'Decode', - cause: 'error updating the stepIndex cookie', - }), - ), - ), - ); - }); + }).pipe(Effect.orDie); + }); }), ); diff --git a/e2e/mock-api-v2/src/middleware/Session.ts b/e2e/mock-api-v2/src/middleware/Session.ts index c04cbec6ee..5cb670608a 100644 --- a/e2e/mock-api-v2/src/middleware/Session.ts +++ b/e2e/mock-api-v2/src/middleware/Session.ts @@ -1,12 +1,16 @@ -import { HttpApiError, HttpApiMiddleware, HttpServerRequest } from '@effect/platform'; +import { HttpApiError, HttpApiMiddleware } from 'effect/unstable/httpapi'; +import * as HttpServerRequest from 'effect/unstable/http/HttpServerRequest'; import { SessionData, SessionStorage } from '../services/session.service.js'; import { Context, Effect, Layer } from 'effect'; +import type { HttpServerResponse } from 'effect/unstable/http/HttpServerResponse'; -class Session extends Context.Tag('Session')() {} +class Session extends Context.Service()('Session') {} -export class SessionMiddleware extends HttpApiMiddleware.Tag()('Session', { - failure: HttpApiError.Unauthorized, - provides: Session, +export class SessionMiddleware extends HttpApiMiddleware.Service< + SessionMiddleware, + { provides: typeof Session } +>()('Session', { + error: HttpApiError.Unauthorized, }) {} export const SessionMiddlewareMock = Layer.effect( @@ -14,27 +18,31 @@ export const SessionMiddlewareMock = Layer.effect( Effect.gen(function* () { const sessionStorage = yield* SessionStorage; - return Effect.gen(function* () { - const request = yield* HttpServerRequest.HttpServerRequest; - const sessionData = yield* sessionStorage - .getSession(request.cookies.sessionId) - .pipe(Effect.orDie); - if (!sessionData) { - const session = yield* sessionStorage.createSession({ - userId: request.cookies.userId, - createdAt: new Date(), - expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour - data: {}, - }); - - return session; - } + return (httpEffect: Effect.Effect) => + Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest; + const sessionData = yield* sessionStorage + .getSession(request.cookies.sessionId) + .pipe(Effect.orDie); - yield* sessionStorage - .refreshSession(request.cookies.sessionId, sessionData.expiresAt) - .pipe(Effect.orDie); + let session: SessionData; + if (!sessionData) { + session = yield* sessionStorage + .createSession({ + userId: request.cookies.userId, + createdAt: new Date(), + expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour + data: {}, + }) + .pipe(Effect.orDie); + } else { + yield* sessionStorage + .refreshSession(request.cookies.sessionId, sessionData.expiresAt) + .pipe(Effect.orDie); + session = sessionData; + } - return sessionData; - }); + return yield* httpEffect.pipe(Effect.provideService(Session, session)); + }); }), ); diff --git a/e2e/mock-api-v2/src/schemas/authorize.schema.ts b/e2e/mock-api-v2/src/schemas/authorize.schema.ts index 470217a5ff..5dddacd0d2 100644 --- a/e2e/mock-api-v2/src/schemas/authorize.schema.ts +++ b/e2e/mock-api-v2/src/schemas/authorize.schema.ts @@ -30,8 +30,7 @@ const _DavinciAuthorizeQuery = Schema.Struct({ }); interface DavinciAuthorizeQuery extends Schema.Schema.Type {} -const DavinciAuthorizeQuery: Schema.Schema = - _DavinciAuthorizeQuery; +const DavinciAuthorizeQuery: Schema.Schema = _DavinciAuthorizeQuery; const DavinciAuthorizeFailure = Schema.Struct({ error: Schema.String, diff --git a/e2e/mock-api-v2/src/schemas/capabilities/capabilities.request.schema.ts b/e2e/mock-api-v2/src/schemas/capabilities/capabilities.request.schema.ts index 83f4c0a0f6..dcb1638258 100644 --- a/e2e/mock-api-v2/src/schemas/capabilities/capabilities.request.schema.ts +++ b/e2e/mock-api-v2/src/schemas/capabilities/capabilities.request.schema.ts @@ -21,7 +21,7 @@ const CapabilitiesRequestBody = Schema.Struct({ interactionId: Schema.String, parameters: Schema.Struct({ eventType: Schema.String, - data: Schema.Union(UsernamePassword), + data: UsernamePassword, }), }); diff --git a/e2e/mock-api-v2/src/schemas/capabilities/capabilities.response.schema.ts b/e2e/mock-api-v2/src/schemas/capabilities/capabilities.response.schema.ts index 8630ef4a85..1f318fcc6c 100644 --- a/e2e/mock-api-v2/src/schemas/capabilities/capabilities.response.schema.ts +++ b/e2e/mock-api-v2/src/schemas/capabilities/capabilities.response.schema.ts @@ -19,7 +19,7 @@ const UsernamePasswordFormData = Schema.Struct({ }), }); -const FormData = Schema.Union(UsernamePasswordFormData, ProtectSDKRequestFormData); +const FormData = Schema.Union([UsernamePasswordFormData, ProtectSDKRequestFormData]); const CapabilitiesResponse = Schema.Struct({ interactionId: Schema.String, diff --git a/e2e/mock-api-v2/src/schemas/open-id-configuration/open-id-configuration-response.schema.ts b/e2e/mock-api-v2/src/schemas/open-id-configuration/open-id-configuration-response.schema.ts index 0768594b09..22899db4f5 100644 --- a/e2e/mock-api-v2/src/schemas/open-id-configuration/open-id-configuration-response.schema.ts +++ b/e2e/mock-api-v2/src/schemas/open-id-configuration/open-id-configuration-response.schema.ts @@ -41,9 +41,7 @@ interface openIdConfigurationResponseSchema extends Schema.Schema.Type< typeof _openIdConfigurationResponseSchema > {} -const openIdConfigurationResponseSchema: Schema.Schema< - openIdConfigurationResponseSchema, - openIdConfigurationResponseSchema -> = _openIdConfigurationResponseSchema; +const openIdConfigurationResponseSchema: Schema.Schema = + _openIdConfigurationResponseSchema; export { _openIdConfigurationResponseSchema, openIdConfigurationResponseSchema }; diff --git a/e2e/mock-api-v2/src/schemas/return-success-response-redirect.schema.ts b/e2e/mock-api-v2/src/schemas/return-success-response-redirect.schema.ts index 9b9294c8dc..fe97f9c899 100644 --- a/e2e/mock-api-v2/src/schemas/return-success-response-redirect.schema.ts +++ b/e2e/mock-api-v2/src/schemas/return-success-response-redirect.schema.ts @@ -43,6 +43,5 @@ const _SuccessResponseRedirect = Schema.Struct({ }); interface SuccessResponseRedirect extends Schema.Schema.Type {} -const SuccessResponseRedirect: Schema.Schema = - _SuccessResponseRedirect; +const SuccessResponseRedirect: Schema.Schema = _SuccessResponseRedirect; export { SuccessResponseRedirect }; diff --git a/e2e/mock-api-v2/src/schemas/revoke/revoke.schema.ts b/e2e/mock-api-v2/src/schemas/revoke/revoke.schema.ts index 7e072bbebf..b363c0bbd1 100644 --- a/e2e/mock-api-v2/src/schemas/revoke/revoke.schema.ts +++ b/e2e/mock-api-v2/src/schemas/revoke/revoke.schema.ts @@ -15,7 +15,7 @@ const RevokePath = Schema.Struct({ const RevokeRequestBody = Schema.Struct({ token: Schema.String, // The token to be revoked token_type_hint: Schema.optional( - Schema.Union(Schema.Literal('access_token'), Schema.Literal('refresh_token')), + Schema.Union([Schema.Literal('access_token'), Schema.Literal('refresh_token')]), ), // Hint about token type (access_token or refresh_token) client_id: Schema.optional(Schema.String), // OAuth 2.0 client identifier client_secret: Schema.optional(Schema.String), // OAuth 2.0 client secret diff --git a/e2e/mock-api-v2/src/schemas/token/token.schema.ts b/e2e/mock-api-v2/src/schemas/token/token.schema.ts index d5701e86f1..c39f8f9c9b 100644 --- a/e2e/mock-api-v2/src/schemas/token/token.schema.ts +++ b/e2e/mock-api-v2/src/schemas/token/token.schema.ts @@ -9,13 +9,13 @@ import { Schema } from 'effect'; const _TokenRequestBody = Schema.Struct({ client_id: Schema.String, code: Schema.String, - grant_type: Schema.Union(Schema.Literal('authorization_code')), + grant_type: Schema.Union([Schema.Literal('authorization_code')]), redirect_uri: Schema.String, code_verifier: Schema.String, }); interface TokenRequestBody extends Schema.Schema.Type {} -const TokenRequestBody: Schema.Schema = _TokenRequestBody; +const TokenRequestBody: Schema.Schema = _TokenRequestBody; const _TokenResponseBody = Schema.Struct({ access_token: Schema.String, @@ -27,6 +27,6 @@ const _TokenResponseBody = Schema.Struct({ }); interface TokenResponseBody extends Schema.Schema.Type {} -const TokenResponseBody: Schema.Schema = _TokenResponseBody; +const TokenResponseBody: Schema.Schema = _TokenResponseBody; export { TokenRequestBody, TokenResponseBody }; diff --git a/e2e/mock-api-v2/src/services/mock-env-helpers/index.ts b/e2e/mock-api-v2/src/services/mock-env-helpers/index.ts index cb93a0616d..d496f48d04 100644 --- a/e2e/mock-api-v2/src/services/mock-env-helpers/index.ts +++ b/e2e/mock-api-v2/src/services/mock-env-helpers/index.ts @@ -12,7 +12,7 @@ import { CapabilitiesResponse } from '../../schemas/capabilities/capabilities.re import { QueryTypes } from '../../types/index.js'; import { validator } from '../../helpers/match.js'; -import { HttpApiError } from '@effect/platform'; +import { HttpApiError } from 'effect/unstable/httpapi'; /** * Given data in the shape of Ping's Request formData.value @@ -34,10 +34,9 @@ const getArrayFromResponseMap = (query: QueryTypes) => * to grab the array from the `responseMap`. */ const getNextStep = (bool: boolean, query: QueryTypes) => - Effect.if(bool, { - onTrue: () => getArrayFromResponseMap(query), - onFalse: () => Effect.fail(new UnableToFindNextStep()), - }); + Effect.suspend(() => + bool ? getArrayFromResponseMap(query) : Effect.fail(new UnableToFindNextStep()), + ); /** * Get the first element in the responseMap @@ -52,27 +51,25 @@ const getFirstElement = (arr: (typeof responseMap)[ResponseMapKeys]) => * */ const getFirstElementAndRespond = (query: QueryTypes) => - pipe( - Option.fromNullable(query?.acr_values), - Option.map((acr) => responseMap[acr as ResponseMapKeys]), - Effect.flatMap(getFirstElement), - Effect.catchTag('NoSuchElementException', () => new HttpApiError.NotFound()), - ); + Effect.gen(function* () { + const acr = query?.acr_values; + if (acr == null) return yield* Effect.fail(new HttpApiError.NotFound()); + const arr = responseMap[acr as ResponseMapKeys]; + if (!arr) return yield* Effect.fail(new HttpApiError.NotFound()); + return yield* getFirstElement(arr); + }); /** * helper function that dives into a request body for Capabilities Response * and will apply a validator function to ensure the request passes validation */ const validateCapabilitiesResponse = (body: any) => - pipe( - body, - Option.fromNullable, - Option.map((body) => body.parameters), - Option.map((parameters) => parameters.data), - Option.map((data) => data.formData), - Option.map((formData) => formData.value), - Effect.flatMap(validator), - ); + Effect.gen(function* () { + if (body == null) return yield* Effect.fail(new HttpApiError.InternalServerError()); + const value = body?.parameters?.data?.formData?.value; + if (value == null) return yield* Effect.fail(new HttpApiError.InternalServerError()); + return yield* validator(value); + }); export { getNextStep, diff --git a/e2e/mock-api-v2/src/services/session.service.ts b/e2e/mock-api-v2/src/services/session.service.ts index 26a3399cef..e4ae3ce317 100644 --- a/e2e/mock-api-v2/src/services/session.service.ts +++ b/e2e/mock-api-v2/src/services/session.service.ts @@ -1,4 +1,4 @@ -import { Effect } from 'effect'; +import { Context, Effect } from 'effect'; import { nanoid } from 'nanoid'; export type SessionData = { @@ -9,98 +9,104 @@ export type SessionData = { }; export interface SessionStorageApi { - createSession: (data: SessionData) => Effect.Effect; - getSession: (sessionId: string) => Effect.Effect; - deleteSession: (sessionId: string) => Effect.Effect; - updateSession: (sessionId: string, data: SessionData) => Effect.Effect; - refreshSession: (sessionId: string, expiryDate?: Date) => Effect.Effect; + createSession: (data: SessionData) => Effect.Effect; + getSession: (sessionId: string) => Effect.Effect; + deleteSession: (sessionId: string) => Effect.Effect; + updateSession: ( + sessionId: string, + data: SessionData, + ) => Effect.Effect; + refreshSession: (sessionId: string, expiryDate?: Date) => Effect.Effect; isSessionExpired: (sessionData: SessionData) => boolean; - cleanupExpiredSessions: () => Effect.Effect; + cleanupExpiredSessions: () => Effect.Effect; } -export class SessionStorage extends Effect.Service()('SessionStorage', { - sync: () => { - // In-memory session store - const _store = new Map(); - - // Check if a session is expired - const isSessionExpired = (sessionData: SessionData): boolean => { - const now = new Date(); - return sessionData.expiresAt < now; - }; - - return { - createSession: Effect.fn('CreateSessionMiddleware')(function* (data: SessionData) { - const sessionId = nanoid(); - _store.set(sessionId, data); - return data; - }), - - getSession: Effect.fn('GetSessionMiddleware')(function* (sessionId: string) { - const session = _store.get(sessionId); - - if (!session) { - return null; - } - - // Check if session is expired - if (isSessionExpired(session)) { - _store.delete(sessionId); - return null; - } - - return session; - }), - - deleteSession: Effect.fn('DeleteSessionMiddleware')(function* (sessionId: string) { - _store.delete(sessionId); - return undefined; - }), - - updateSession: Effect.fn('UpdateSessionMiddleware')(function* ( - sessionId: string, - data: SessionData, - ) { - if (!_store.has(sessionId)) { - return new Error('Session not found'); - } - _store.set(sessionId, data); - return data; - }), - - refreshSession: Effect.fn('RefreshSessionMiddleware')(function* ( - sessionId: string, - expiryDate?: Date, - ) { - const session = _store.get(sessionId); - - if (!session) { - return Effect.fail(new Error('Session not found')); - } - - if (isSessionExpired(session)) { - _store.delete(sessionId); - return new Error('Session has expired'); - } +export class SessionStorage extends Context.Service()( + 'SessionStorage', + { + make: Effect.sync(() => { + // In-memory session store + const _store = new Map(); + + // Check if a session is expired + const isSessionExpired = (sessionData: SessionData): boolean => { + const now = new Date(); + return sessionData.expiresAt < now; + }; + + return { + createSession: Effect.fn('CreateSessionMiddleware')(function* (data: SessionData) { + const sessionId = nanoid(); + _store.set(sessionId, data); + return data; + }), + + getSession: Effect.fn('GetSessionMiddleware')(function* (sessionId: string) { + const session = _store.get(sessionId); + + if (!session) { + return null; + } - // Update expiry date - const newExpiryDate = expiryDate || new Date(Date.now() + 24 * 60 * 60 * 1000); // Default: 24 hours from now - session.expiresAt = newExpiryDate; - _store.set(sessionId, session); + // Check if session is expired + if (isSessionExpired(session)) { + _store.delete(sessionId); + return null; + } - return session.data; - }), + return session; + }), - isSessionExpired, + deleteSession: Effect.fn('DeleteSessionMiddleware')(function* (sessionId: string) { + _store.delete(sessionId); + return undefined; + }), + + updateSession: Effect.fn('UpdateSessionMiddleware')(function* ( + sessionId: string, + data: SessionData, + ) { + if (!_store.has(sessionId)) { + return new Error('Session not found'); + } + _store.set(sessionId, data); + return data; + }), + + refreshSession: Effect.fn('RefreshSessionMiddleware')(function* ( + sessionId: string, + expiryDate?: Date, + ) { + const session = _store.get(sessionId); + + if (!session) { + return Effect.fail(new Error('Session not found')); + } - cleanupExpiredSessions: Effect.fn('CleanupExpiredSessionsMiddleware')(function* () { - for (const [sessionId, session] of _store.entries()) { if (isSessionExpired(session)) { _store.delete(sessionId); + return new Error('Session has expired'); + } + + // Update expiry date + const newExpiryDate = expiryDate || new Date(Date.now() + 24 * 60 * 60 * 1000); // Default: 24 hours from now + session.expiresAt = newExpiryDate; + _store.set(sessionId, session); + + return session.data; + }), + + isSessionExpired, + + cleanupExpiredSessions: Effect.fn('CleanupExpiredSessionsMiddleware')(function* () { + for (const [sessionId, session] of _store.entries()) { + if (isSessionExpired(session)) { + _store.delete(sessionId); + } } - } - return undefined; - }), - }; + return undefined; + }), + }; + }), }, -}) {} +) {} diff --git a/e2e/mock-api-v2/src/services/tokens.service.ts b/e2e/mock-api-v2/src/services/tokens.service.ts index 5c2df0c5b5..8d63924dbf 100644 --- a/e2e/mock-api-v2/src/services/tokens.service.ts +++ b/e2e/mock-api-v2/src/services/tokens.service.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ import { Context, Effect, Layer, Schema } from 'effect'; -import { HttpApiError } from '@effect/platform'; +import { HttpApiError } from 'effect/unstable/httpapi'; import { tokenResponseBody } from '../responses/token/token.js'; import { TokenResponseBody } from '../schemas/token/token.schema.js'; import { revokeResponseBody } from '../responses/revoke/revoke.js'; @@ -16,7 +16,7 @@ import { HeaderTypes } from '../types/index.js'; type TokensResponseBody = Schema.Schema.Type; type RevokeTokenResponseBody = Schema.Schema.Type; -class Tokens extends Context.Tag('@services/Tokens')< +class Tokens extends Context.Service< Tokens, { getTokens: ( @@ -27,31 +27,28 @@ class Tokens extends Context.Tag('@services/Tokens')< tokenTypeHint?: string, ) => Effect.Effect; } ->() {} +>()('@services/Tokens') {} -const TokensMock = Layer.succeed( - Tokens, - Tokens.of({ - getTokens: () => - Effect.gen(function* () { - const response = yield* Effect.tryPromise({ - try: () => Promise.resolve(tokenResponseBody), - catch: () => new HttpApiError.Unauthorized(), - }); - return response; - }), - revokeToken: (token) => - Effect.gen(function* () { - // Apply the REVOKED_ prefix to the token - // This is a simple way to mark tokens as revoked without maintaining state - // The Authorization middleware will check for this prefix - yield* Effect.log('Revoking token', { token, newToken: `REVOKED_${token}` }); +const TokensMock = Layer.succeed(Tokens, { + getTokens: () => + Effect.gen(function* () { + const response = yield* Effect.tryPromise({ + try: () => Promise.resolve(tokenResponseBody), + catch: () => new HttpApiError.Unauthorized(), + }); + return response; + }), + revokeToken: (token) => + Effect.gen(function* () { + // Apply the REVOKED_ prefix to the token + // This is a simple way to mark tokens as revoked without maintaining state + // The Authorization middleware will check for this prefix + yield* Effect.log('Revoking token', { token, newToken: `REVOKED_${token}` }); - // In a real implementation, we might store the token in a revocation list - // or update it in a database. For this mock, we'll just return success. - return revokeResponseBody; - }), - }), -); + // In a real implementation, we might store the token in a revocation list + // or update it in a database. For this mock, we'll just return success. + return revokeResponseBody; + }), +}); export { TokensMock, Tokens }; diff --git a/e2e/mock-api-v2/src/services/userinfo.service.ts b/e2e/mock-api-v2/src/services/userinfo.service.ts index b9e3dabc45..2b359d5980 100644 --- a/e2e/mock-api-v2/src/services/userinfo.service.ts +++ b/e2e/mock-api-v2/src/services/userinfo.service.ts @@ -4,12 +4,12 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { Layer, Schema } from 'effect'; -import { Effect, Context } from 'effect'; +import { Context, Layer, Schema } from 'effect'; +import { Effect } from 'effect'; import { userInfoResponse } from '../responses/userinfo/userinfo.js'; import { UserInfoSchema } from '../schemas/userinfo/userinfo.schema.js'; -import { HttpApiError } from '@effect/platform'; +import { HttpApiError } from 'effect/unstable/httpapi'; /*** * This file should be converted to a Layer that uses Request @@ -17,25 +17,22 @@ import { HttpApiError } from '@effect/platform'; type UserInfoResponse = Schema.Schema.Type; -class UserInfo extends Context.Tag('@services/userinfo')< +class UserInfo extends Context.Service< UserInfo, { getUserInfo: ( token: string, ) => Effect.Effect; } ->() {} +>()('@services/userinfo') {} -const UserInfoMockService = Layer.succeed( - UserInfo, - UserInfo.of({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getUserInfo: (_: string) => - Effect.tryPromise({ - try: () => Promise.resolve(userInfoResponse), - catch: () => new HttpApiError.Unauthorized(), - }), - }), -); +const UserInfoMockService = Layer.succeed(UserInfo, { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getUserInfo: (_: string) => + Effect.tryPromise({ + try: () => Promise.resolve(userInfoResponse), + catch: () => new HttpApiError.Unauthorized(), + }), +}); export { UserInfo, UserInfoMockService }; diff --git a/e2e/mock-api-v2/src/spec.ts b/e2e/mock-api-v2/src/spec.ts index 3981a26464..4ae313b64c 100644 --- a/e2e/mock-api-v2/src/spec.ts +++ b/e2e/mock-api-v2/src/spec.ts @@ -5,7 +5,13 @@ * of the MIT license. See the LICENSE file for details. */ import { Schema } from 'effect'; -import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from '@effect/platform'; +import { + HttpApi, + HttpApiEndpoint, + HttpApiError, + HttpApiGroup, + OpenApi, +} from 'effect/unstable/httpapi'; import { openIdConfigurationResponseSchema } from './schemas/open-id-configuration/open-id-configuration-response.schema.js'; import { TokenResponseBody } from './schemas/token/token.schema.js'; import { UserInfoSchema } from './schemas/userinfo/userinfo.schema.js'; @@ -45,8 +51,9 @@ const MockApi = HttpApi.make('MyApi') // Healthcheck .add( HttpApiGroup.make('Healthcheck').add( - HttpApiEndpoint.get('HealthCheck')`/healthcheck` - .addSuccess(Schema.String) + HttpApiEndpoint.get('HealthCheck', '/healthcheck', { + success: Schema.String, + }) .annotate(OpenApi.Summary, 'Server Health Check') .annotate( OpenApi.Description, @@ -57,13 +64,13 @@ const MockApi = HttpApi.make('MyApi') // Authorization .add( HttpApiGroup.make('Authorization').add( - HttpApiEndpoint.get('authorize', `/:envid/davinci/authorize`) - .setPath(Schema.Struct({ envid: Schema.String })) - .setHeaders(DavinciAuthorizeHeaders) - .setUrlParams(DavinciAuthorizeQuery) - .addSuccess(CapabilitiesResponse) - .addError(HttpApiError.NotFound) - .addError(HttpApiError.InternalServerError) + HttpApiEndpoint.get('authorize', `/:envid/davinci/authorize`, { + params: Schema.Struct({ envid: Schema.String }), + headers: DavinciAuthorizeHeaders, + query: DavinciAuthorizeQuery, + success: CapabilitiesResponse, + error: [HttpApiError.NotFound, HttpApiError.InternalServerError], + }) .annotate(OpenApi.Summary, 'Authorization Endpoint') .annotate( OpenApi.Description, @@ -78,23 +85,27 @@ const MockApi = HttpApi.make('MyApi') HttpApiEndpoint.post( 'capabilities', `/:envid/davinci/connections/:connectionID/capabilities/:capabilityName`, - ) - .setPayload(CapabilitiesRequestBody) - .setPath(CapabilitiesPathParams) - - .setHeaders(CapabilitiesHeaders) - .addSuccess(CapabilitiesResponse) - .addError(HttpApiError.NotFound) - .addError(HttpApiError.Unauthorized) - .addError(HttpApiError.InternalServerError), + { + payload: CapabilitiesRequestBody, + params: CapabilitiesPathParams, + headers: CapabilitiesHeaders, + success: CapabilitiesResponse, + error: [ + HttpApiError.NotFound, + HttpApiError.Unauthorized, + HttpApiError.InternalServerError, + ], + }, + ), ) .middleware(IncrementStepIndex), ) .add( HttpApiGroup.make('OpenIDConfig').add( - HttpApiEndpoint.get('openid', `/:envid/as/.well-known/openid-configuration`) - .setPath(Schema.Struct({ envid: Schema.String })) - .addSuccess(openIdConfigurationResponseSchema) + HttpApiEndpoint.get('openid', `/:envid/as/.well-known/openid-configuration`, { + params: Schema.Struct({ envid: Schema.String }), + success: openIdConfigurationResponseSchema, + }) .annotate(OpenApi.Summary, 'OIDC Configuration') .annotate( OpenApi.Description, @@ -106,10 +117,11 @@ const MockApi = HttpApi.make('MyApi') .add( HttpApiGroup.make('Tokens') .add( - HttpApiEndpoint.post('Tokens', `/:envid/as/token`) - .addSuccess(TokenResponseBody) - .addError(HttpApiError.Unauthorized) - .setPath(Schema.Struct({ envid: Schema.String })) + HttpApiEndpoint.post('Tokens', `/:envid/as/token`, { + params: Schema.Struct({ envid: Schema.String }), + success: TokenResponseBody, + error: HttpApiError.Unauthorized, + }) .annotate(OpenApi.Summary, 'Token Endpoint') .annotate( OpenApi.Description, @@ -123,10 +135,11 @@ const MockApi = HttpApi.make('MyApi') .add( HttpApiGroup.make('ProtectedRequests') .add( - HttpApiEndpoint.get('UserInfo', `/:envid/as/userinfo`) - .setPath(Schema.Struct({ envid: Schema.String })) - .addSuccess(UserInfoSchema) - .addError(HttpApiError.Unauthorized) + HttpApiEndpoint.get('UserInfo', `/:envid/as/userinfo`, { + params: Schema.Struct({ envid: Schema.String }), + success: UserInfoSchema, + error: HttpApiError.Unauthorized, + }) .annotate(OpenApi.Summary, 'UserInfo Endpoint') .annotate( OpenApi.Description, @@ -140,21 +153,20 @@ const MockApi = HttpApi.make('MyApi') .add( HttpApiGroup.make('SessionManagement') .add( - HttpApiEndpoint.get('EndSession', `/:envid/as/endSession`) - .setPath(EndSessionPath) - .setUrlParams(EndSessionQuery) - .setHeaders(EndSessionHeaders) - .addSuccess( - Schema.Union( - Schema.String, - Schema.Struct({ - status: Schema.Number, - headers: Schema.Record({ key: Schema.String, value: Schema.String }), - body: Schema.String, - }), - ), - ) - .addError(HttpApiError.Unauthorized) + HttpApiEndpoint.get('EndSession', `/:envid/as/endSession`, { + params: EndSessionPath, + query: EndSessionQuery, + headers: EndSessionHeaders, + success: Schema.Union([ + Schema.String, + Schema.Struct({ + status: Schema.Number, + headers: Schema.Record(Schema.String, Schema.String), + body: Schema.String, + }), + ]), + error: HttpApiError.Unauthorized, + }) .annotate(OpenApi.Summary, 'End Session Endpoint') .annotate( OpenApi.Description, @@ -164,31 +176,15 @@ const MockApi = HttpApi.make('MyApi') .middleware(Authorization) .middleware(SessionMiddleware), ) - // Protected Requests - .add( - HttpApiGroup.make('ProtectedRequests') - .add( - HttpApiEndpoint.get('UserInfo', `/:envid/as/userinfo`) - .setPath(Schema.Struct({ envid: Schema.String })) - .addSuccess(UserInfoSchema) - .addError(HttpApiError.Unauthorized) - .annotate(OpenApi.Summary, 'UserInfo Endpoint') - .annotate( - OpenApi.Description, - 'Returns claims about the authenticated end-user. Requires a valid access token.', - ), - ) - .middleware(Authorization) - .middleware(SessionMiddleware), - ) .add( HttpApiGroup.make('Revoke') .add( - HttpApiEndpoint.post('RevokeToken', `/:envid/as/revoke`) - .setPath(RevokePath) - .setPayload(RevokeRequestBody) - .addSuccess(RevokeResponseBody) - .addError(HttpApiError.Unauthorized) + HttpApiEndpoint.post('RevokeToken', `/:envid/as/revoke`, { + params: RevokePath, + payload: RevokeRequestBody, + success: RevokeResponseBody, + error: HttpApiError.Unauthorized, + }) .annotate(OpenApi.Summary, 'Token Revocation Endpoint') .annotate( OpenApi.Description, @@ -196,7 +192,7 @@ const MockApi = HttpApi.make('MyApi') ), ) .middleware(Authorization) - .middleware(SessionMiddleware), // Applies to token, end session, revoke + .middleware(SessionMiddleware), ) // Middlewares for relevant endpoints .annotate( diff --git a/package.json b/package.json index a3be4a1a82..c16211c6b8 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "@commitlint/cli": "^20.0.0", "@commitlint/config-conventional": "^20.0.0", "@commitlint/prompt": "^20.0.0", - "@effect/cli": "catalog:effect", "@eslint/eslintrc": "^3.0.0", "@eslint/js": "~9.39.0", "@evilmartians/lefthook": "^2.1.4", @@ -85,7 +84,7 @@ "@typescript-eslint/typescript-estree": "8.23.0", "@typescript-eslint/utils": "^8.13.0", "@vitest/coverage-v8": "catalog:vitest", - "@vitest/ui": "3.2.6", + "@vitest/ui": "catalog:vitest", "conventional-changelog-conventionalcommits": "^8.0.0", "cz-conventional-changelog": "^3.3.0", "cz-git": "^1.6.1", diff --git a/packages/davinci-client/api-report/davinci-client.api.md b/packages/davinci-client/api-report/davinci-client.api.md index aec288bc28..11525d978f 100644 --- a/packages/davinci-client/api-report/davinci-client.api.md +++ b/packages/davinci-client/api-report/davinci-client.api.md @@ -287,13 +287,11 @@ export function davinci(input: { resume: (input: { continueToken: string; }) => Promise; - start: (options?: StartOptions | undefined) => Promise; + start: (options?: StartOptions | undefined) => Promise; update: (collector: T) => Updater; validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; pollStatus: (collector: PollingCollector) => Poller; getClient: () => { - status: "start"; - } | { action: string; collectors: Collectors[]; description?: string; @@ -305,19 +303,21 @@ export function davinci(input: { description?: string; name?: string; status: "error"; + } | { + status: "failure"; + } | { + status: "start"; } | { authorization?: { code?: string; state?: string; }; status: "success"; - } | { - status: "failure"; } | null; getCollectors: () => Collectors[]; getError: () => DaVinciError | null; getErrorCollectors: () => CollectorErrors[]; - getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; + getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode; getServer: () => { _links?: Links; id?: string; @@ -326,8 +326,6 @@ export function davinci(input: { href?: string; eventName?: string; status: "continue"; - } | { - status: "start"; } | { _links?: Links; eventName?: string; @@ -338,20 +336,22 @@ export function davinci(input: { } | { _links?: Links; eventName?: string; + href?: string; id?: string; interactionId?: string; interactionToken?: string; - href?: string; - session?: string; - status: "success"; + status: "failure"; + } | { + status: "start"; } | { _links?: Links; eventName?: string; - href?: string; id?: string; interactionId?: string; interactionToken?: string; - status: "failure"; + href?: string; + session?: string; + status: "success"; } | null; cache: { getLatestResponse: () => ({ diff --git a/packages/davinci-client/api-report/davinci-client.types.api.md b/packages/davinci-client/api-report/davinci-client.types.api.md index 88f48561f2..4a5ef2492f 100644 --- a/packages/davinci-client/api-report/davinci-client.types.api.md +++ b/packages/davinci-client/api-report/davinci-client.types.api.md @@ -287,13 +287,11 @@ export function davinci(input: { resume: (input: { continueToken: string; }) => Promise; - start: (options?: StartOptions | undefined) => Promise; + start: (options?: StartOptions | undefined) => Promise; update: (collector: T) => Updater; validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; pollStatus: (collector: PollingCollector) => Poller; getClient: () => { - status: "start"; - } | { action: string; collectors: Collectors[]; description?: string; @@ -305,19 +303,21 @@ export function davinci(input: { description?: string; name?: string; status: "error"; + } | { + status: "failure"; + } | { + status: "start"; } | { authorization?: { code?: string; state?: string; }; status: "success"; - } | { - status: "failure"; } | null; getCollectors: () => Collectors[]; getError: () => DaVinciError | null; getErrorCollectors: () => CollectorErrors[]; - getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; + getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode; getServer: () => { _links?: Links; id?: string; @@ -326,8 +326,6 @@ export function davinci(input: { href?: string; eventName?: string; status: "continue"; - } | { - status: "start"; } | { _links?: Links; eventName?: string; @@ -338,20 +336,22 @@ export function davinci(input: { } | { _links?: Links; eventName?: string; + href?: string; id?: string; interactionId?: string; interactionToken?: string; - href?: string; - session?: string; - status: "success"; + status: "failure"; + } | { + status: "start"; } | { _links?: Links; eventName?: string; - href?: string; id?: string; interactionId?: string; interactionToken?: string; - status: "failure"; + href?: string; + session?: string; + status: "success"; } | null; cache: { getLatestResponse: () => ({ diff --git a/packages/davinci-client/src/lib/client.store.effects.test.ts b/packages/davinci-client/src/lib/client.store.effects.test.ts index 365118aac0..aaa821b399 100644 --- a/packages/davinci-client/src/lib/client.store.effects.test.ts +++ b/packages/davinci-client/src/lib/client.store.effects.test.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ -import { Micro } from 'effect'; +import { Effect, Exit } from 'effect'; import { describe, expect, vi } from 'vitest'; import { it } from '@effect/vitest'; @@ -185,7 +185,7 @@ describe('getPollingModeµ', () => { }; it.effect('succeeds with challenge mode when challenge and pollChallengeStatus are set', () => - Micro.gen(function* () { + Effect.gen(function* () { const collector: PollingCollector = { ...basePollingCollector, output: { @@ -206,7 +206,7 @@ describe('getPollingModeµ', () => { ); it.effect('succeeds with continue mode when no challenge is present', () => - Micro.gen(function* () { + Effect.gen(function* () { const result = yield* getPollingModeµ(basePollingCollector); expect(result).toStrictEqual({ @@ -218,7 +218,7 @@ describe('getPollingModeµ', () => { ); it.effect('succeeds with unknown mode for ambiguous configuration', () => - Micro.gen(function* () { + Effect.gen(function* () { const collector: PollingCollector = { ...basePollingCollector, output: { @@ -239,13 +239,13 @@ describe('getPollingModeµ', () => { ); it.effect('fails when collector type is not PollingCollector', () => - Micro.gen(function* () { + Effect.gen(function* () { const badCollector = { ...basePollingCollector, type: 'TextCollector' } as any; - const result = yield* Micro.exit(getPollingModeµ(badCollector)); + const result = yield* Effect.exit(getPollingModeµ(badCollector)); expect(result).toStrictEqual( - Micro.exitFail({ + Exit.fail({ error: { message: 'Collector provided to poll is not a PollingCollector', type: 'argument_error', @@ -257,7 +257,7 @@ describe('getPollingModeµ', () => { ); it.effect('fails when retriesRemaining is undefined in continue mode', () => - Micro.gen(function* () { + Effect.gen(function* () { const collector: PollingCollector = { ...basePollingCollector, output: { @@ -266,10 +266,10 @@ describe('getPollingModeµ', () => { }, }; - const result = yield* Micro.exit(getPollingModeµ(collector)); + const result = yield* Effect.exit(getPollingModeµ(collector)); expect(result).toStrictEqual( - Micro.exitFail({ + Exit.fail({ error: { message: 'No retries found on PollingCollector', type: 'argument_error', diff --git a/packages/davinci-client/src/lib/client.store.effects.ts b/packages/davinci-client/src/lib/client.store.effects.ts index de97bc0c39..00165bbb31 100644 --- a/packages/davinci-client/src/lib/client.store.effects.ts +++ b/packages/davinci-client/src/lib/client.store.effects.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ -import { Micro } from 'effect'; +import { Effect } from 'effect'; import { SerializedError } from '@reduxjs/toolkit/react'; import { FetchBaseQueryError } from '@reduxjs/toolkit/query/react'; @@ -14,6 +14,7 @@ import type { logger as loggerFn } from '@forgerock/sdk-logger'; import type { DavinciStore, RootState } from './client.store.utils.js'; import type { PollingStatus, InternalErrorResponse } from './client.types.js'; import type { PollingCollector } from './collector.types.js'; +import type { ContinueNode } from './node.types.js'; import { createInternalError, isInternalError } from './client.store.utils.js'; import { davinciApi } from './davinci.api.js'; @@ -55,9 +56,9 @@ const isRecord = (value: unknown): value is Record => */ export function getPollingModeµ( collector: PollingCollector, -): Micro.Micro { +): Effect.Effect { if (collector.type !== 'PollingCollector') { - return Micro.fail( + return Effect.fail( createInternalError('Collector provided to poll is not a PollingCollector', 'argument_error'), ); } @@ -66,23 +67,23 @@ export function getPollingModeµ( collector.output.config; if (challenge && pollChallengeStatus === true) { - return Micro.succeed({ _tag: 'challenge', challenge }); + return Effect.succeed({ _tag: 'challenge', challenge }); } if (!challenge && !pollChallengeStatus) { if (retriesRemaining === undefined) { - return Micro.fail( + return Effect.fail( createInternalError('No retries found on PollingCollector', 'argument_error'), ); } - return Micro.succeed({ + return Effect.succeed({ _tag: 'continue', retriesRemaining, pollInterval: pollInterval ?? 2000, }); } - return Micro.succeed({ _tag: 'unknown' }); + return Effect.succeed({ _tag: 'unknown' }); } /** @@ -114,16 +115,16 @@ export function buildChallengeEndpoint( } /** - * Lifts a selector result with { error, state } shape into a Micro. + * Lifts a selector result with { error, state } shape into an Effect. * Succeeds with state when error is null, fails with InternalErrorResponse otherwise. */ function fromSelectorµ(result: { error: { message: string } | null; state: T; -}): Micro.Micro, InternalErrorResponse> { +}): Effect.Effect, InternalErrorResponse> { return result.error - ? Micro.fail(createInternalError(result.error.message, 'state_error')) - : Micro.succeed(result.state as NonNullable); + ? Effect.fail(createInternalError(result.error.message, 'state_error')) + : Effect.succeed(result.state as NonNullable); } /** @@ -133,35 +134,35 @@ function fromSelectorµ(result: { export function validatePollingPrerequisitesµ( rootState: RootState, challenge: string, -): Micro.Micro { +): Effect.Effect { if (!challenge) { - return Micro.fail( + return Effect.fail( createInternalError('No challenge found on collector for poll operation', 'state_error'), ); } return fromSelectorµ(nodeSlice.selectors.selectContinueServer(rootState)).pipe( - Micro.filterOrFail( - (server) => !!server.interactionId, + Effect.filterOrFail( + (server: ContinueNode['server']) => !!server.interactionId, () => createInternalError( 'Missing interactionId in server info for challenge polling', 'state_error', ), ), - Micro.flatMap((server) => + Effect.flatMap((server: ContinueNode['server']) => fromSelectorµ(nodeSlice.selectors.selectSelfLink(rootState)).pipe( - Micro.map((selfLink) => ({ server, selfLink })), + Effect.map((selfLink) => ({ server, selfLink })), ), ), - Micro.flatMap(({ server, selfLink }) => { + Effect.flatMap(({ server, selfLink }: { server: ContinueNode['server']; selfLink: string }) => { const endpoint = buildChallengeEndpoint(selfLink, challenge); return typeof endpoint === 'string' - ? Micro.succeed({ + ? Effect.succeed({ interactionId: server.interactionId!, challengeEndpoint: endpoint, }) - : Micro.fail(endpoint); + : Effect.fail(endpoint); }), ); } @@ -222,14 +223,14 @@ export function interpretChallengeResponse( return pollStatus ? (pollStatus as PollingStatus) : 'error'; } - // If we reach here, Micro.repeat exhausted its schedule without the challenge completing + // If we reach here, the poll loop exhausted its retries without the challenge completing log.debug('Challenge polling timed out'); return 'timedOut'; } /** - * Builds a Micro effect for the challenge polling branch. - * validate → dispatch → repeat → interpret → lift errors + * Builds an Effect for the challenge polling branch. + * validate → dispatch initial → loop (sleep + re-dispatch) while pending → interpret */ function challengePollingµ({ collector, @@ -241,51 +242,60 @@ function challengePollingµ({ challenge: string; store: DavinciStore; log: ReturnType; -}): Micro.Micro { +}): Effect.Effect { const maxRetries = collector.output.config.pollRetries ?? 60; const pollInterval = collector.output.config.pollInterval ?? 2000; - return validatePollingPrerequisitesµ(store.getState(), challenge).pipe( - Micro.flatMap(({ interactionId, challengeEndpoint }) => - Micro.promise(() => + return Effect.gen(function* () { + const { interactionId, challengeEndpoint } = yield* validatePollingPrerequisitesµ( + store.getState(), + challenge, + ); + + const doPoll = (): Effect.Effect => + Effect.promise(() => store.dispatch( davinciApi.endpoints.poll.initiate({ endpoint: challengeEndpoint, interactionId, }), ), - ), - ), - Micro.repeat({ - while: isChallengeStillPending, - // `times` tracks repetitions after the initial attempt, so decrement by one - times: maxRetries - 1, - schedule: Micro.scheduleSpaced(pollInterval), - }), - Micro.map((response) => interpretChallengeResponse(response, log)), - Micro.flatMap((result) => - isInternalError(result) ? Micro.fail(result) : Micro.succeed(result), - ), - ); + ); + + let response: PollDispatchResult = yield* doPoll(); + + for (let i = 0; i < maxRetries - 1 && isChallengeStillPending(response); i++) { + yield* Effect.sleep(pollInterval); + response = yield* doPoll(); + } + + const status = interpretChallengeResponse(response, log); + + if (isInternalError(status)) { + return yield* Effect.fail(status); + } + + return status; + }); } /** - * Builds a Micro effect for the continue polling branch. + * Builds an Effect for the continue polling branch. * If retries remain, delays by pollInterval then returns 'continue'. * If retries are exhausted, returns 'timedOut' immediately. */ function continuePollingµ( mode: Extract, -): Micro.Micro { +): Effect.Effect { if (mode.retriesRemaining <= 0) { - return Micro.succeed('timedOut' as PollingStatus); + return Effect.succeed('timedOut' as PollingStatus); } - return Micro.sleep(mode.pollInterval).pipe(Micro.map(() => 'continue')); + return Effect.sleep(mode.pollInterval).pipe(Effect.map(() => 'continue' as PollingStatus)); } /** * Routes a validated PollingMode to the appropriate polling effect. - * This is the single entry point — the caller lifts getPollingMode into Micro, pipes through this. + * This is the single entry point — the caller lifts getPollingMode into Effect, pipes through this. */ export function pollingµ({ mode, @@ -297,7 +307,7 @@ export function pollingµ({ collector: PollingCollector; store: DavinciStore; log: ReturnType; -}): Micro.Micro { +}): Effect.Effect { if (mode._tag === 'challenge') { return challengePollingµ({ collector, challenge: mode.challenge, store, log }); } @@ -306,7 +316,7 @@ export function pollingµ({ return continuePollingµ(mode); } - return Micro.fail( + return Effect.fail( createInternalError('Invalid polling collector configuration', 'argument_error'), ); } diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index ee180a9cd8..c70f163234 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -4,8 +4,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { Micro } from 'effect'; -import { exitIsFail, exitIsSuccess } from 'effect/Micro'; +import { Effect, Exit, Cause, Option } from 'effect'; import { type CustomLogger, logger as loggerFn, type LogLevel } from '@forgerock/sdk-logger'; import { createStorage } from '@forgerock/storage'; import { isGenericError, createWellknownError } from '@forgerock/sdk-utilities'; @@ -20,7 +19,7 @@ import { toSdkStore, type RootState, } from './client.store.utils.js'; -import { pollingµ, getPollingModeµ } from './client.store.effects.js'; +import { pollingµ, getPollingModeµ, type PollingMode } from './client.store.effects.js'; import { nodeSlice } from './node.slice.js'; import { davinciApi } from './davinci.api.js'; import { configSlice } from './config.slice.js'; @@ -440,17 +439,22 @@ export async function davinci({ pollStatus: (collector: PollingCollector): Poller => { return async () => { const result = await getPollingModeµ(collector).pipe( - Micro.flatMap((mode) => pollingµ({ mode, collector, store, log })), - Micro.tapError((err) => Micro.sync(() => log.error(err.error.message))), - Micro.runPromiseExit, + Effect.flatMap((mode: PollingMode) => pollingµ({ mode, collector, store, log })), + Effect.tapError((err: InternalErrorResponse) => + Effect.sync(() => log.error(err.error.message)), + ), + Effect.runPromiseExit, ); - if (exitIsSuccess(result)) { + if (Exit.isSuccess(result)) { return result.value; } - if (exitIsFail(result)) { - return result.cause.error; + if (Exit.isFailure(result)) { + const maybeError = Cause.findErrorOption(result.cause); + if (Option.isSome(maybeError)) { + return maybeError.value; + } } return createInternalError( diff --git a/packages/davinci-client/src/lib/fido/fido.ts b/packages/davinci-client/src/lib/fido/fido.ts index 2434012d21..1b96ca85d8 100644 --- a/packages/davinci-client/src/lib/fido/fido.ts +++ b/packages/davinci-client/src/lib/fido/fido.ts @@ -4,8 +4,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { Micro } from 'effect'; -import { exitIsFail, exitIsSuccess } from 'effect/Micro'; +import { Effect, Exit, Cause, Option } from 'effect'; import { toFidoErrorCode, @@ -46,14 +45,14 @@ export function fido(): FidoClient { ); } - const createCredentialµ = Micro.sync(() => transformRegistrationOptions(options)).pipe( - Micro.flatMap((publicKeyCredentialCreationOptions) => - Micro.tryPromise({ + const createCredentialµ = Effect.sync(() => transformRegistrationOptions(options)).pipe( + Effect.flatMap((publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions) => + Effect.tryPromise({ try: () => navigator.credentials.create({ publicKey: publicKeyCredentialCreationOptions, }), - catch: (error) => { + catch: (error: unknown) => { const code = toFidoErrorCode(error); console.error('Failed to create keypair: ', code); return createFidoError( @@ -64,9 +63,9 @@ export function fido(): FidoClient { }, }), ), - Micro.flatMap((credential) => { + Effect.flatMap((credential: Credential | null) => { if (!credential) { - return Micro.fail( + return Effect.fail( createFidoError( 'UnknownError', 'registration_error', @@ -74,19 +73,25 @@ export function fido(): FidoClient { ), ); } - return Micro.succeed(transformPublicKeyCredential(credential as PublicKeyCredential)); + return Effect.succeed(transformPublicKeyCredential(credential as PublicKeyCredential)); }), ); - const result = await Micro.runPromiseExit(createCredentialµ); + const result = await Effect.runPromiseExit(createCredentialµ); - if (exitIsSuccess(result)) { + if (Exit.isSuccess(result)) { return result.value; - } else if (exitIsFail(result)) { - return result.cause.error; - } else { - return createFidoError('UnknownError', 'registration_error', result.cause.message); } + + if (Exit.isFailure(result)) { + const maybeError = Cause.findErrorOption(result.cause); + if (Option.isSome(maybeError)) { + return maybeError.value; + } + return createFidoError('UnknownError', 'registration_error', Cause.pretty(result.cause)); + } + + return createFidoError('UnknownError', 'registration_error', 'Unexpected exit state'); }, /** @@ -103,14 +108,14 @@ export function fido(): FidoClient { ); } - const getAssertionµ = Micro.sync(() => transformAuthenticationOptions(options)).pipe( - Micro.flatMap((publicKeyCredentialRequestOptions) => - Micro.tryPromise({ + const getAssertionµ = Effect.sync(() => transformAuthenticationOptions(options)).pipe( + Effect.flatMap((publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions) => + Effect.tryPromise({ try: () => navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions, }), - catch: (error) => { + catch: (error: unknown) => { const code = toFidoErrorCode(error); console.error('Failed to authenticate: ', code); return createFidoError( @@ -121,9 +126,9 @@ export function fido(): FidoClient { }, }), ), - Micro.flatMap((assertion) => { + Effect.flatMap((assertion: Credential | null) => { if (!assertion) { - return Micro.fail( + return Effect.fail( createFidoError( 'UnknownError', 'authentication_error', @@ -131,19 +136,25 @@ export function fido(): FidoClient { ), ); } - return Micro.succeed(transformAssertion(assertion as PublicKeyCredential)); + return Effect.succeed(transformAssertion(assertion as PublicKeyCredential)); }), ); - const result = await Micro.runPromiseExit(getAssertionµ); + const result = await Effect.runPromiseExit(getAssertionµ); - if (exitIsSuccess(result)) { + if (Exit.isSuccess(result)) { return result.value; - } else if (exitIsFail(result)) { - return result.cause.error; - } else { - return createFidoError('UnknownError', 'authentication_error', result.cause.message); } + + if (Exit.isFailure(result)) { + const maybeError = Cause.findErrorOption(result.cause); + if (Option.isSome(maybeError)) { + return maybeError.value; + } + return createFidoError('UnknownError', 'authentication_error', Cause.pretty(result.cause)); + } + + return createFidoError('UnknownError', 'authentication_error', 'Unexpected exit state'); }, }; } diff --git a/packages/davinci-client/src/lib/password-policy.rules.ts b/packages/davinci-client/src/lib/password-policy.rules.ts index 2d6f517fe5..7a74d91616 100644 --- a/packages/davinci-client/src/lib/password-policy.rules.ts +++ b/packages/davinci-client/src/lib/password-policy.rules.ts @@ -4,7 +4,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { Array as Arr, Option, pipe } from 'effect'; +import { Array as Arr, Result, pipe } from 'effect'; import type { ValidatedPasswordCollector } from './collector.types.js'; import type { PasswordPolicy } from './davinci.types.js'; @@ -66,8 +66,8 @@ const minCharactersRule: PasswordPolicyRule = (policy, value) => { let hits = 0; for (const ch of value) if (members.has(ch)) hits += 1; return hits < min - ? Option.some(`Password must contain at least ${min} character(s) from "${charset}"`) - : Option.none(); + ? Result.succeed(`Password must contain at least ${min} character(s) from "${charset}"`) + : Result.failVoid; }), ); }; diff --git a/packages/journey-client/src/lib/client.store.ts b/packages/journey-client/src/lib/client.store.ts index 5ffa627c93..dd66152a4d 100644 --- a/packages/journey-client/src/lib/client.store.ts +++ b/packages/journey-client/src/lib/client.store.ts @@ -20,7 +20,7 @@ import { createJourneyStore, toSdkStore } from './client.store.utils.js'; import { configSlice } from './config.slice.js'; import { journeyApi } from './journey.api.js'; import { createStorage } from '@forgerock/storage'; -import * as Either from 'effect/Either'; +import * as Result from 'effect/Result'; import { createJourneyObject, parseJourneyResponse } from './journey.utils.js'; import type { JourneyResult } from './journey.utils.js'; import { wellknownApi } from '@forgerock/sdk-wellknown'; @@ -161,9 +161,9 @@ export async function journey({ start: async (options?: StartParam) => { const response = await store.dispatch(journeyApi.endpoints.start.initiate(options)); - return Either.match(parseJourneyResponse(response), { - onLeft: (err): JourneyResult => err, - onRight: (step): JourneyResult => createJourneyObject(step), + return Result.match(parseJourneyResponse(response), { + onFailure: (err): JourneyResult => err, + onSuccess: (step): JourneyResult => createJourneyObject(step), }); }, @@ -172,9 +172,9 @@ export async function journey({ */ next: async (step: JourneyStep, options?: NextOptions) => { const response = await store.dispatch(journeyApi.endpoints.next.initiate({ step, options })); - return Either.match(parseJourneyResponse(response), { - onLeft: (err): JourneyResult => err, - onRight: (step): JourneyResult => createJourneyObject(step), + return Result.match(parseJourneyResponse(response), { + onFailure: (err): JourneyResult => err, + onSuccess: (step): JourneyResult => createJourneyObject(step), }); }, diff --git a/packages/journey-client/src/lib/journey.utils.test.ts b/packages/journey-client/src/lib/journey.utils.test.ts index 30718d8159..a271c8bfc8 100644 --- a/packages/journey-client/src/lib/journey.utils.test.ts +++ b/packages/journey-client/src/lib/journey.utils.test.ts @@ -70,8 +70,8 @@ describe('parseJourneyResponse', () => { const result = parseJourneyResponse({ data: undefined, error }); - expect(result._tag).toBe('Right'); - expect((result as { right: unknown }).right).toBe(body); + expect(result._tag).toBe('Success'); + expect((result as { success: unknown }).success).toBe(body); }); it('returns left(GenericError) when FetchBaseQueryError has numeric status but non-object body', () => { @@ -79,8 +79,8 @@ describe('parseJourneyResponse', () => { const result = parseJourneyResponse({ data: undefined, error }); - expect(result._tag).toBe('Left'); - expect((result as { left: unknown }).left).toMatchObject({ + expect(result._tag).toBe('Failure'); + expect((result as { failure: unknown }).failure).toMatchObject({ error: 'request_failed', type: 'unknown_error', }); @@ -91,8 +91,8 @@ describe('parseJourneyResponse', () => { const result = parseJourneyResponse({ data: undefined, error }); - expect(result._tag).toBe('Left'); - expect((result as { left: { message: string } }).left.message).toContain('Network error'); + expect(result._tag).toBe('Failure'); + expect((result as { failure: { message: string } }).failure.message).toContain('Network error'); }); it('returns left(GenericError) for PARSING_ERROR', () => { @@ -105,8 +105,10 @@ describe('parseJourneyResponse', () => { const result = parseJourneyResponse({ data: undefined, error }); - expect(result._tag).toBe('Left'); - expect((result as { left: { message: string } }).left.message).toContain('JSON parse error'); + expect(result._tag).toBe('Failure'); + expect((result as { failure: { message: string } }).failure.message).toContain( + 'JSON parse error', + ); }); it('returns left(GenericError) for TIMEOUT_ERROR', () => { @@ -114,8 +116,10 @@ describe('parseJourneyResponse', () => { const result = parseJourneyResponse({ data: undefined, error }); - expect(result._tag).toBe('Left'); - expect((result as { left: { message: string } }).left.message).toContain('Request timed out'); + expect(result._tag).toBe('Failure'); + expect((result as { failure: { message: string } }).failure.message).toContain( + 'Request timed out', + ); }); it('returns left(GenericError) for CUSTOM_ERROR', () => { @@ -123,8 +127,8 @@ describe('parseJourneyResponse', () => { const result = parseJourneyResponse({ data: undefined, error }); - expect(result._tag).toBe('Left'); - expect((result as { left: { message: string } }).left.message).toContain( + expect(result._tag).toBe('Failure'); + expect((result as { failure: { message: string } }).failure.message).toContain( 'Custom error occurred', ); }); @@ -134,8 +138,8 @@ describe('parseJourneyResponse', () => { const result = parseJourneyResponse({ data: undefined, error }); - expect(result._tag).toBe('Left'); - expect((result as { left: { message: string } }).left.message).toContain( + expect(result._tag).toBe('Failure'); + expect((result as { failure: { message: string } }).failure.message).toContain( 'Something went wrong', ); }); @@ -143,8 +147,8 @@ describe('parseJourneyResponse', () => { it('returns left(GenericError) when no data and no error', () => { const result = parseJourneyResponse({ data: undefined, error: undefined }); - expect(result._tag).toBe('Left'); - expect((result as { left: unknown }).left).toMatchObject({ + expect(result._tag).toBe('Failure'); + expect((result as { failure: unknown }).failure).toMatchObject({ error: 'no_response_data', type: 'unknown_error', }); @@ -155,7 +159,7 @@ describe('parseJourneyResponse', () => { const result = parseJourneyResponse({ data, error: undefined }); - expect(result._tag).toBe('Right'); - expect((result as { right: unknown }).right).toBe(data); + expect(result._tag).toBe('Success'); + expect((result as { success: unknown }).success).toBe(data); }); }); diff --git a/packages/journey-client/src/lib/journey.utils.ts b/packages/journey-client/src/lib/journey.utils.ts index 198721e43e..3605ce05c7 100644 --- a/packages/journey-client/src/lib/journey.utils.ts +++ b/packages/journey-client/src/lib/journey.utils.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ -import * as Either from 'effect/Either'; +import * as Result from 'effect/Result'; import { StepType } from '@forgerock/sdk-types'; @@ -71,7 +71,7 @@ export function createJourneyObject( export function parseJourneyResponse(res: { data?: Step; error?: FetchBaseQueryError | SerializedError; -}): Either.Either { +}): Result.Result { // https://redux-toolkit.js.org/rtk-query/api/fetchBaseQuery#signature // AM sends LoginFailure as a structured step body over HTTP 4xx — normalise both sources so the // left guards below only see genuine transport failures, never AM application responses. @@ -88,7 +88,7 @@ export function parseJourneyResponse(res: { // https://redux-toolkit.js.org/rtk-query/usage-with-typescript#type-safe-error-handling // Non-HTTP fetch failure (network down, CORS, etc.) — definitely left, never carries an AM body if (res.error && 'error' in res.error) { - return Either.left({ + return Result.fail({ error: 'request_failed', message: `Request failed: ${res.error.error}`, type: 'unknown_error', @@ -97,7 +97,7 @@ export function parseJourneyResponse(res: { // Redux serialization error — definitely left, never carries an AM body if (res.error && 'message' in res.error) { - return Either.left({ + return Result.fail({ error: 'request_failed', message: `Request failed: ${res.error.message ?? 'Unknown error'}`, type: 'unknown_error', @@ -106,7 +106,7 @@ export function parseJourneyResponse(res: { // HTTP error whose body was not a parseable AM step — left if (res.error && !stepData) { - return Either.left({ + return Result.fail({ error: 'request_failed', message: 'Request failed: Unknown error', type: 'unknown_error', @@ -115,7 +115,7 @@ export function parseJourneyResponse(res: { // No data from either source — left if (!stepData) { - return Either.left({ + return Result.fail({ error: 'no_response_data', message: 'No data received from server', type: 'unknown_error', @@ -123,5 +123,5 @@ export function parseJourneyResponse(res: { } // Every transport failure has been ruled out — this is a valid AM step - return Either.right(stepData); + return Result.succeed(stepData); } diff --git a/packages/oidc-client/src/lib/authorize.request.micros.test.ts b/packages/oidc-client/src/lib/authorize.request.micros.test.ts index 289617eae9..6a40bbd4df 100644 --- a/packages/oidc-client/src/lib/authorize.request.micros.test.ts +++ b/packages/oidc-client/src/lib/authorize.request.micros.test.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ import { it, expect } from '@effect/vitest'; -import { Micro } from 'effect'; +import { Cause, Effect, Exit, Option } from 'effect'; import { vi, afterEach } from 'vitest'; import * as sdkOidc from '@forgerock/sdk-oidc'; import * as sdkUtilities from '@forgerock/sdk-utilities'; @@ -61,7 +61,7 @@ afterEach(() => { // ─── generateAuthValuesµ ─────────────────────────────────────────────────────── it.effect('generateAuthValuesµ returns auth URL options and store function', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.stubGlobal('sessionStorage', sessionStorageStub); const result = yield* generateAuthValuesµ(config, wellknown); const [opts, storeFn] = result; @@ -73,22 +73,24 @@ it.effect('generateAuthValuesµ returns auth URL options and store function', () ); it.effect('generateAuthValuesµ fails with auth_error when sessionStorage throws', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.spyOn(sdkOidc, 'generateAndStoreAuthUrlValues').mockImplementation(() => { throw new Error('storage unavailable'); }); - const exit = yield* Micro.exit(generateAuthValuesµ(config, wellknown)); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; - expect(exit.cause.error.type).toBe('auth_error'); - expect(exit.cause.error.error).toBe('PAR_PARAM_BUILD_ERROR'); + const exit = yield* Effect.exit(generateAuthValuesµ(config, wellknown)); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('auth_error'); + expect(errorOpt.value.error).toBe('PAR_PARAM_BUILD_ERROR'); }), ); // ─── generatePkceChallengeµ ──────────────────────────────────────────────────── it.effect('generatePkceChallengeµ returns a non-empty challenge string', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.stubGlobal('crypto', { subtle: { digest: vi.fn().mockResolvedValue(new ArrayBuffer(32)), @@ -102,20 +104,22 @@ it.effect('generatePkceChallengeµ returns a non-empty challenge string', () => ); it.effect('generatePkceChallengeµ fails with auth_error when createChallenge throws', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.spyOn(sdkUtilities, 'createChallenge').mockRejectedValue(new Error('crypto unavailable')); - const exit = yield* Micro.exit(generatePkceChallengeµ('bad-verifier')); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; - expect(exit.cause.error.type).toBe('auth_error'); - expect(exit.cause.error.error).toBe('PAR_CHALLENGE_ERROR'); + const exit = yield* Effect.exit(generatePkceChallengeµ('bad-verifier')); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('auth_error'); + expect(errorOpt.value.error).toBe('PAR_CHALLENGE_ERROR'); }), ); // ─── buildParBodyµ ───────────────────────────────────────────────────────────── it.effect('buildParBodyµ returns URLSearchParams with expected fields', () => - Micro.gen(function* () { + Effect.gen(function* () { const params = yield* buildParBodyµ(config, {}, 'challenge-abc', 'state-xyz'); expect(params.get('client_id')).toBe(clientId); expect(params.get('code_challenge')).toBe('challenge-abc'); @@ -126,29 +130,31 @@ it.effect('buildParBodyµ returns URLSearchParams with expected fields', () => ); it.effect('buildParBodyµ includes prompt when provided', () => - Micro.gen(function* () { + Effect.gen(function* () { const params = yield* buildParBodyµ(config, {}, 'challenge-abc', 'state-xyz', 'login'); expect(params.get('prompt')).toBe('login'); }), ); it.effect('buildParBodyµ fails with auth_error when buildAuthorizeParams throws', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.spyOn(sdkOidc, 'buildAuthorizeParams').mockImplementation(() => { throw new Error('build failed'); }); - const exit = yield* Micro.exit(buildParBodyµ(config, {}, 'ch', 'st')); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; - expect(exit.cause.error.type).toBe('auth_error'); - expect(exit.cause.error.error).toBe('PAR_PARAM_BUILD_ERROR'); + const exit = yield* Effect.exit(buildParBodyµ(config, {}, 'ch', 'st')); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('auth_error'); + expect(errorOpt.value.error).toBe('PAR_PARAM_BUILD_ERROR'); }), ); // ─── buildParSlimUrlµ ────────────────────────────────────────────────────────── it.effect('buildParSlimUrlµ returns URL with only client_id and request_uri', () => - Micro.gen(function* () { + Effect.gen(function* () { const url = yield* buildParSlimUrlµ( wellknown.authorization_endpoint, clientId, @@ -162,7 +168,7 @@ it.effect('buildParSlimUrlµ returns URL with only client_id and request_uri', ( ); it.effect('buildParSlimUrlµ includes prompt when provided', () => - Micro.gen(function* () { + Effect.gen(function* () { const url = yield* buildParSlimUrlµ( wellknown.authorization_endpoint, clientId, @@ -176,7 +182,7 @@ it.effect('buildParSlimUrlµ includes prompt when provided', () => // ─── storeAuthOptionsµ ───────────────────────────────────────────────────────── it.effect('storeAuthOptionsµ calls the provided store function', () => - Micro.gen(function* () { + Effect.gen(function* () { const storeFn = vi.fn(); yield* storeAuthOptionsµ(storeFn); expect(storeFn).toHaveBeenCalledOnce(); @@ -184,23 +190,25 @@ it.effect('storeAuthOptionsµ calls the provided store function', () => ); it.effect('storeAuthOptionsµ fails with unknown_error when store function throws', () => - Micro.gen(function* () { - const exit = yield* Micro.exit( + Effect.gen(function* () { + const exit = yield* Effect.exit( storeAuthOptionsµ(() => { throw new Error('storage write failed'); }), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; - expect(exit.cause.error.type).toBe('unknown_error'); - expect(exit.cause.error.error).toBe('PAR_STORAGE_ERROR'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('unknown_error'); + expect(errorOpt.value.error).toBe('PAR_STORAGE_ERROR'); }), ); // ─── validateParResponseµ ────────────────────────────────────────────────────── it.effect('validateParResponseµ succeeds when request_uri is present', () => - Micro.gen(function* () { + Effect.gen(function* () { const result = yield* validateParResponseµ({ data: { request_uri: 'urn:ietf:params:oauth:request_uri:xyz', expires_in: 60 }, }); @@ -209,8 +217,8 @@ it.effect('validateParResponseµ succeeds when request_uri is present', () => ); it.effect('validateParResponseµ fails with network_error on RTK error', () => - Micro.gen(function* () { - const exit = yield* Micro.exit( + Effect.gen(function* () { + const exit = yield* Effect.exit( validateParResponseµ({ error: { status: 400, @@ -218,26 +226,30 @@ it.effect('validateParResponseµ fails with network_error on RTK error', () => }, }), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; - expect(exit.cause.error.error).toBe('invalid_client'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('invalid_client'); }), ); it.effect('validateParResponseµ fails with network_error when request_uri is absent', () => - Micro.gen(function* () { - const exit = yield* Micro.exit(validateParResponseµ({ data: { expires_in: 60 } })); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; - expect(exit.cause.error.type).toBe('network_error'); - expect(exit.cause.error.error_description).toContain('request_uri'); + Effect.gen(function* () { + const exit = yield* Effect.exit(validateParResponseµ({ data: { expires_in: 60 } })); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('network_error'); + expect(errorOpt.value.error_description).toContain('request_uri'); }), ); // ─── createAuthorizeUrlµ ────────────────────────────────────────────────── it.effect('createAuthorizeUrlµ returns [url, options] tuple', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.stubGlobal('sessionStorage', sessionStorageStub); const opts = { clientId, @@ -257,9 +269,9 @@ it.effect('createAuthorizeUrlµ returns [url, options] tuple', () => ); it.effect('createAuthorizeUrlµ fails with auth_error when createAuthorizeUrl rejects', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.spyOn(sdkOidc, 'createAuthorizeUrl').mockRejectedValue(new Error('url build failed')); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( createAuthorizeUrlµ(wellknown.authorization_endpoint, { clientId, redirectUri, @@ -267,18 +279,20 @@ it.effect('createAuthorizeUrlµ fails with auth_error when createAuthorizeUrl re responseType, }), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; - expect(exit.cause.error.type).toBe('auth_error'); - expect(exit.cause.error.error).toBe('AuthorizationUrlError'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('auth_error'); + expect(errorOpt.value.error).toBe('AuthorizationUrlError'); }), ); // ─── handleDispatchErrorµ ────────────────────────────────────────────────────── it.effect('handleDispatchErrorµ fails immediately for CONFIGURATION_ERROR', () => - Micro.gen(function* () { - const exit = yield* Micro.exit( + Effect.gen(function* () { + const exit = yield* Effect.exit( handleDispatchErrorµ( { status: 'CUSTOM_ERROR', @@ -290,18 +304,20 @@ it.effect('handleDispatchErrorµ fails immediately for CONFIGURATION_ERROR', () { clientId, redirectUri, scope, responseType }, ), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; - expect(exit.cause.error.type).toBe('unknown_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('unknown_error'); }), ); it.effect('handleDispatchErrorµ builds redirect URL for non-config errors', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.spyOn(sdkOidc, 'createAuthorizeUrl').mockResolvedValue( 'https://example.com/authorize?error=login_required', ); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( handleDispatchErrorµ( { status: 400, @@ -315,17 +331,19 @@ it.effect('handleDispatchErrorµ builds redirect URL for non-config errors', () { clientId, redirectUri, scope, responseType }, ), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; - expect(exit.cause.error.error).toBe('login_required'); - expect(exit.cause.error).toHaveProperty('redirectUrl'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('login_required'); + expect(errorOpt.value).toHaveProperty('redirectUrl'); }), ); // ─── dispatchAuthorizeFetchµ ─────────────────────────────────────────────────── it.effect('dispatchAuthorizeFetchµ succeeds with authorizeResponse', () => - Micro.gen(function* () { + Effect.gen(function* () { const authorizeResponse = { code: 'auth-code-abc', state: 'state-xyz' }; vi.mocked(mockStore.dispatch).mockResolvedValueOnce({ data: { authorizeResponse }, @@ -344,12 +362,12 @@ it.effect('dispatchAuthorizeFetchµ succeeds with authorizeResponse', () => it.effect( 'dispatchAuthorizeFetchµ fails with unknown_error when data has no authorizeResponse', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.mocked(mockStore.dispatch).mockResolvedValueOnce({ data: {}, } as unknown as ReturnType); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( dispatchAuthorizeFetchµ(mockStore, 'https://example.com/authorize', wellknown, { clientId, redirectUri, @@ -357,16 +375,18 @@ it.effect( responseType, }), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; - expect(exit.cause.error.type).toBe('unknown_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('unknown_error'); }), ); // ─── dispatchAuthorizeIframeµ ────────────────────────────────────────────────── it.effect('dispatchAuthorizeIframeµ succeeds with iframe data', () => - Micro.gen(function* () { + Effect.gen(function* () { const iframeData = { code: 'iframe-code', state: 'state-abc' }; vi.mocked(mockStore.dispatch).mockResolvedValueOnce({ data: iframeData, @@ -383,12 +403,12 @@ it.effect('dispatchAuthorizeIframeµ succeeds with iframe data', () => ); it.effect('dispatchAuthorizeIframeµ fails with unknown_error when data is undefined', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.mocked(mockStore.dispatch).mockResolvedValueOnce({ data: undefined, } as unknown as ReturnType); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( dispatchAuthorizeIframeµ(mockStore, 'https://example.com/authorize', wellknown, { clientId, redirectUri, @@ -396,19 +416,21 @@ it.effect('dispatchAuthorizeIframeµ fails with unknown_error when data is undef responseType, }), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; - expect(exit.cause.error.type).toBe('unknown_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('unknown_error'); }), ); it.effect('dispatchAuthorizeIframeµ fails with unknown_error when data has no code or state', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.mocked(mockStore.dispatch).mockResolvedValueOnce({ data: { unexpected: 'shape' }, } as unknown as ReturnType); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( dispatchAuthorizeIframeµ( mockStore, 'https://example.com/authorize?foo=bar', @@ -416,9 +438,11 @@ it.effect('dispatchAuthorizeIframeµ fails with unknown_error when data has no c {} as import('@forgerock/sdk-types').GetAuthorizationUrlOptions, ), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; - expect(exit.cause.error.type).toBe('unknown_error'); - expect(exit.cause.error.error).toBe('Unknown_Error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('unknown_error'); + expect(errorOpt.value.error).toBe('Unknown_Error'); }), ); diff --git a/packages/oidc-client/src/lib/authorize.request.micros.ts b/packages/oidc-client/src/lib/authorize.request.micros.ts index 43db85a0cf..bc3be03ed8 100644 --- a/packages/oidc-client/src/lib/authorize.request.micros.ts +++ b/packages/oidc-client/src/lib/authorize.request.micros.ts @@ -10,7 +10,7 @@ import { generateAndStoreAuthUrlValues, } from '@forgerock/sdk-oidc'; import { createChallenge } from '@forgerock/sdk-utilities'; -import { Micro } from 'effect'; +import { Effect } from 'effect'; import { buildParAuthorizeUrl, @@ -39,8 +39,8 @@ export const generateAuthValuesµ = ( config: OidcConfig, wellknown: WellknownResponse, options?: OptionalAuthorizeOptions, -): Micro.Micro, AuthorizationError, never> => { - return Micro.try({ +): Effect.Effect, AuthorizationError, never> => { + return Effect.try({ try: () => generateAndStoreAuthUrlValues({ clientId: config.clientId, @@ -60,8 +60,8 @@ export const generateAuthValuesµ = ( export const generatePkceChallengeµ = ( verifier: string, -): Micro.Micro => { - return Micro.tryPromise({ +): Effect.Effect => { + return Effect.tryPromise({ try: () => createChallenge(verifier), catch: (err): AuthorizationError => ({ error: 'PAR_CHALLENGE_ERROR', @@ -73,8 +73,8 @@ export const generatePkceChallengeµ = ( export const storeAuthOptionsµ = ( storeOptions: () => void, -): Micro.Micro => { - return Micro.try({ +): Effect.Effect => { + return Effect.try({ try: () => storeOptions(), catch: (err): AuthorizationError => ({ error: 'PAR_STORAGE_ERROR', @@ -92,8 +92,8 @@ export const buildParBodyµ = ( challenge: string, state: string, prompt?: AuthPromptValue, -): Micro.Micro => { - return Micro.try({ +): Effect.Effect => { + return Effect.try({ try: () => buildAuthorizeParams({ clientId: config.clientId, @@ -116,8 +116,8 @@ export const buildParBodyµ = ( export const createAuthorizeUrlµ = ( path: string, options: GetAuthorizationUrlOptions, -): Micro.Micro<[string, GetAuthorizationUrlOptions], AuthorizationError, never> => { - return Micro.tryPromise({ +): Effect.Effect<[string, GetAuthorizationUrlOptions], AuthorizationError, never> => { + return Effect.tryPromise({ try: async () => [await createAuthorizeUrl(path, { ...options, prompt: 'none' }), options] as [ string, @@ -136,8 +136,8 @@ export const buildAuthorizeRedirectUrlµ = ( res: { error: string; error_description: string }, wellknown: WellknownResponse, options: GetAuthorizationUrlOptions, -): Micro.Micro => { - return Micro.tryPromise({ +): Effect.Effect => { + return Effect.tryPromise({ try: () => createAuthorizeUrl(wellknown.authorization_endpoint, { ...options }), catch: (error): AuthorizationError => ({ error: 'AuthorizationUrlError', @@ -146,8 +146,8 @@ export const buildAuthorizeRedirectUrlµ = ( type: 'auth_error', }), }).pipe( - Micro.flatMap((url) => - Micro.fail({ + Effect.flatMap((url) => + Effect.fail({ error: res.error, error_description: res.error_description, type: 'auth_error', @@ -160,26 +160,26 @@ export const buildAuthorizeRedirectUrlµ = ( export const validateParResponseµ = (result: { error?: FetchBaseQueryError | SerializedError; data?: unknown; -}): Micro.Micro<{ request_uri: string; expires_in: number }, AuthorizationError, never> => { +}): Effect.Effect<{ request_uri: string; expires_in: number }, AuthorizationError, never> => { if (result.error) { - return Micro.fail(toDispatchError(result.error)); + return Effect.fail(toDispatchError(result.error)); } if (!hasPushRequestUri(result.data)) { - return Micro.fail({ + return Effect.fail({ error: 'PAR_ERROR', error_description: "PAR response missing required 'request_uri' field", type: 'network_error', } as const); } const d = result.data as { request_uri: string; expires_in?: number }; - return Micro.succeed({ request_uri: d.request_uri, expires_in: d.expires_in ?? 60 }); + return Effect.succeed({ request_uri: d.request_uri, expires_in: d.expires_in ?? 60 }); }; export const handleDispatchErrorµ = ( error: FetchBaseQueryError | SerializedError, wellknown: WellknownResponse, options: GetAuthorizationUrlOptions, -): Micro.Micro => { +): Effect.Effect => { const errorDetails = toDispatchError(error); const isConfigError = isFetchBaseQueryError(error) && @@ -187,7 +187,7 @@ export const handleDispatchErrorµ = ( error.statusText === 'CONFIGURATION_ERROR'; return isConfigError - ? Micro.fail(errorDetails) + ? Effect.fail(errorDetails) : buildAuthorizeRedirectUrlµ(errorDetails, wellknown, options); }; @@ -197,12 +197,12 @@ export const dispatchParRequestµ = ( store: ClientStore, parEndpoint: string, body: URLSearchParams, -): Micro.Micro< +): Effect.Effect< { error?: FetchBaseQueryError | SerializedError; data?: unknown }, AuthorizationError, never > => { - return Micro.tryPromise({ + return Effect.tryPromise({ try: () => store.dispatch(oidcApi.endpoints.par.initiate({ endpoint: parEndpoint, body })), catch: (error): AuthorizationError => ({ error: 'PAR_DISPATCH_ERROR', @@ -217,8 +217,8 @@ export const buildParSlimUrlµ = ( clientId: string, requestUri: string, prompt?: AuthPromptValue, -): Micro.Micro => { - return Micro.try({ +): Effect.Effect => { + return Effect.try({ try: () => buildParAuthorizeUrl({ authorizationEndpoint, clientId, requestUri, prompt }), catch: (err): AuthorizationError => ({ error: 'PAR_URL_BUILD_ERROR', @@ -235,8 +235,8 @@ export const dispatchAuthorizeFetchµ = ( url: string, wellknown: WellknownResponse, options: GetAuthorizationUrlOptions, -): Micro.Micro => { - return Micro.tryPromise({ +): Effect.Effect => { + return Effect.tryPromise({ try: () => store.dispatch(oidcApi.endpoints.authorizeFetch.initiate({ url })), catch: (error): AuthorizationError => ({ error: 'AUTHORIZE_DISPATCH_ERROR', @@ -245,14 +245,14 @@ export const dispatchAuthorizeFetchµ = ( type: 'network_error', }), }).pipe( - Micro.flatMap(({ error, data }) => { + Effect.flatMap(({ error, data }) => { if (error) { return handleDispatchErrorµ(error, wellknown, options); } if (data?.authorizeResponse) { - return Micro.succeed(data.authorizeResponse); + return Effect.succeed(data.authorizeResponse); } - return Micro.fail({ + return Effect.fail({ error: 'Unknown_Error', error_description: 'Response schema was not recognized', type: 'unknown_error', @@ -266,8 +266,8 @@ export const dispatchAuthorizeIframeµ = ( url: string, wellknown: WellknownResponse, options: GetAuthorizationUrlOptions, -): Micro.Micro => { - return Micro.tryPromise({ +): Effect.Effect => { + return Effect.tryPromise({ try: () => store.dispatch(oidcApi.endpoints.authorizeIframe.initiate({ url })), catch: (error): AuthorizationError => ({ error: 'AUTHORIZE_DISPATCH_ERROR', @@ -276,15 +276,15 @@ export const dispatchAuthorizeIframeµ = ( type: 'network_error', }), }).pipe( - Micro.flatMap(({ error, data }) => { + Effect.flatMap(({ error, data }) => { if (error) { return handleDispatchErrorµ(error, wellknown, options); } const d = data as { code?: unknown; state?: unknown } | undefined; if (d !== undefined && typeof d.code === 'string' && typeof d.state === 'string') { - return Micro.succeed(d as AuthorizationSuccess); + return Effect.succeed(d as AuthorizationSuccess); } - return Micro.fail({ + return Effect.fail({ error: 'Unknown_Error', error_description: 'Response data did not contain expected code and state fields', type: 'unknown_error', diff --git a/packages/oidc-client/src/lib/authorize.request.ts b/packages/oidc-client/src/lib/authorize.request.ts index 826ce8ec51..e458e6e0e4 100644 --- a/packages/oidc-client/src/lib/authorize.request.ts +++ b/packages/oidc-client/src/lib/authorize.request.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ import { CustomLogger } from '@forgerock/sdk-logger'; -import { Micro } from 'effect'; +import { Effect } from 'effect'; import { buildAuthorizeOptions } from './authorize.request.utils.js'; import { @@ -43,16 +43,20 @@ function dispatchAuthorizeµ( wellknown: WellknownResponse, store: ClientStore, log: CustomLogger, -): Micro.Micro { +): Effect.Effect { if (options.responseMode === 'pi.flow') { const { responseMode: _, ...redirectOptions } = options; return dispatchAuthorizeFetchµ(store, url, wellknown, redirectOptions).pipe( - Micro.tap(() => log.debug('Received success response from authorize fetch endpoint')), + Effect.tap(() => + Effect.sync(() => log.debug('Received success response from authorize fetch endpoint')), + ), ); } return dispatchAuthorizeIframeµ(store, url, wellknown, options).pipe( - Micro.tap(() => log.debug('Received success response from authorize iframe endpoint')), + Effect.tap(() => + Effect.sync(() => log.debug('Received success response from authorize iframe endpoint')), + ), ); } @@ -72,7 +76,7 @@ function dispatchAuthorizeµ( * @param options - Optional request-level overrides; `prompt` is split out * so it appears on the slim URL while the rest of the params go in the * PAR POST body. - * @returns A `Micro` that resolves to the slim authorize URL string or + * @returns An `Effect` that resolves to the slim authorize URL string or * fails with a typed `AuthorizationError`. */ export function createParAuthorizeUrlµ( @@ -81,11 +85,11 @@ export function createParAuthorizeUrlµ( log: CustomLogger, store: ClientStore, options?: OptionalAuthorizeOptions, -): Micro.Micro { +): Effect.Effect { const parEndpoint = wellknown.pushed_authorization_request_endpoint; if (!parEndpoint) { - return Micro.fail({ + return Effect.fail({ error: 'PAR_NOT_CONFIGURED', error_description: 'PAR endpoint not found in server configuration', type: 'wellknown_error', @@ -94,7 +98,7 @@ export function createParAuthorizeUrlµ( const { prompt, ...parBodyOptions } = options ?? {}; - return Micro.gen(function* () { + return Effect.gen(function* () { const [authUrlOptions, storeOptions] = yield* generateAuthValuesµ(config, wellknown, options); const challenge = yield* generatePkceChallengeµ(authUrlOptions.verifier); const body = yield* buildParBodyµ( @@ -107,7 +111,7 @@ export function createParAuthorizeUrlµ( const parResult = yield* dispatchParRequestµ(store, parEndpoint, body); const { request_uri, expires_in } = yield* validateParResponseµ(parResult); if (expires_in < 30) { - yield* Micro.sync(() => + yield* Effect.sync(() => log.warn( `PAR request_uri expires in ${expires_in}s — authorize must complete before expiry`, ), @@ -143,7 +147,7 @@ export function createParAuthorizeUrlµ( * indicating whether to use the PAR flow. The caller owns the derivation * logic (`config.par ?? require_pushed_authorization_requests === true`); * this function simply routes on the resolved value. - * @returns A `Micro` that resolves to an `AuthorizationSuccess` containing + * @returns An `Effect` that resolves to an `AuthorizationSuccess` containing * the `code` and `state`, or fails with a typed `AuthorizationError`. */ export function authorizeµ( @@ -153,7 +157,7 @@ export function authorizeµ( store: ClientStore, options: GetAuthorizationUrlOptions | undefined, useParFlow: boolean, -): Micro.Micro { +): Effect.Effect { const parDispatchOptions: GetAuthorizationUrlOptions = { clientId: config.clientId, redirectUri: config.redirectUri, @@ -163,14 +167,14 @@ export function authorizeµ( }; const parFlow = createParAuthorizeUrlµ(wellknown, config, log, store, options).pipe( - Micro.tap((url) => log.debug('PAR authorize URL created', url)), - Micro.tapError((err) => - Micro.sync(() => log.error(`PAR authorize failed [${err.type}]: ${err.error}`, err)), + Effect.tap((url) => Effect.sync(() => log.debug('PAR authorize URL created', url))), + Effect.tapError((err) => + Effect.sync(() => log.error(`PAR authorize failed [${err.type}]: ${err.error}`, err)), ), - Micro.flatMap((url) => + Effect.flatMap((url) => dispatchAuthorizeµ(url, parDispatchOptions, wellknown, store, log).pipe( - Micro.tapError((err) => - Micro.sync(() => log.error('Error dispatching PAR authorize request', err)), + Effect.tapError((err) => + Effect.sync(() => log.error('Error dispatching PAR authorize request', err)), ), ), ), @@ -178,13 +182,13 @@ export function authorizeµ( const [path, opts] = buildAuthorizeOptions(wellknown, config, options); const standardFlow = createAuthorizeUrlµ(path, opts).pipe( - Micro.tap(([url]) => log.debug('Authorize URL created', url)), - Micro.tapError((err) => Micro.sync(() => log.error('Error creating authorize URL', err))), - Micro.flatMap(([url, dispatchOpts]) => + Effect.tap(([url]) => Effect.sync(() => log.debug('Authorize URL created', url))), + Effect.tapError((err) => Effect.sync(() => log.error('Error creating authorize URL', err))), + Effect.flatMap(([url, dispatchOpts]) => dispatchAuthorizeµ(url, dispatchOpts, wellknown, store, log), ), - Micro.tapError((err) => - Micro.sync(() => log.error('Error dispatching authorize request', err)), + Effect.tapError((err) => + Effect.sync(() => log.error('Error dispatching authorize request', err)), ), ); diff --git a/packages/oidc-client/src/lib/authorize.request.utils.test.ts b/packages/oidc-client/src/lib/authorize.request.utils.test.ts index d267eb593f..8b185c10a7 100644 --- a/packages/oidc-client/src/lib/authorize.request.utils.test.ts +++ b/packages/oidc-client/src/lib/authorize.request.utils.test.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ import { it } from '@effect/vitest'; -import { Micro } from 'effect'; +import { Cause, Effect, Exit, Option } from 'effect'; import { vi, afterEach, expect } from 'vitest'; import * as sdkOidc from '@forgerock/sdk-oidc'; import { createParAuthorizeUrlµ, authorizeµ } from './authorize.request.js'; @@ -66,6 +66,7 @@ const sessionStorageStub = { getItem: vi.fn(), setItem: vi.fn(), removeItem: vi. afterEach(() => { vi.unstubAllGlobals(); + vi.clearAllMocks(); vi.restoreAllMocks(); }); @@ -131,26 +132,23 @@ it('toDispatchError delegates to toAuthorizationError for FetchBaseQueryError', // ─── createParAuthorizeUrlµ ─────────────────────────────────────────────────────────── it.effect('createParAuthorizeUrlµ fails with PAR_NOT_CONFIGURED when par endpoint is missing', () => - Micro.gen(function* () { + Effect.gen(function* () { const configWithPar: OidcConfig = { ...config, par: true }; - const result = yield* Micro.exit( + const result = yield* Effect.exit( createParAuthorizeUrlµ(wellknown, configWithPar, mockLog, mockStore), ); - expect(Micro.exitIsFailure(result)).toBe(true); - if (!Micro.exitIsFailure(result)) return; - expect(Micro.causeIsFail(result.cause)).toBe(true); - if (Micro.causeIsFail(result.cause)) { - expect(result.cause.error.error).toBe('PAR_NOT_CONFIGURED'); - expect(result.cause.error.type).toBe('wellknown_error'); - expect(result.cause.error.error_description).toBe( - 'PAR endpoint not found in server configuration', - ); - } + expect(Exit.isFailure(result)).toBe(true); + if (!Exit.isFailure(result)) return; + const errorOpt = Cause.findErrorOption(result.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('PAR_NOT_CONFIGURED'); + expect(errorOpt.value.type).toBe('wellknown_error'); + expect(errorOpt.value.error_description).toBe('PAR endpoint not found in server configuration'); }), ); it.effect('createParAuthorizeUrlµ succeeds and returns slim authorize URL', () => - Micro.gen(function* () { + Effect.gen(function* () { const configWithPar: OidcConfig = { ...config, par: true }; const requestUri = 'urn:ietf:params:oauth:request_uri:abc123'; @@ -170,7 +168,7 @@ it.effect('createParAuthorizeUrlµ succeeds and returns slim authorize URL', () ); it.effect('createParAuthorizeUrlµ fails with network_error when PAR POST returns error', () => - Micro.gen(function* () { + Effect.gen(function* () { const configWithPar: OidcConfig = { ...config, par: true }; vi.stubGlobal('sessionStorage', sessionStorageStub); @@ -182,16 +180,15 @@ it.effect('createParAuthorizeUrlµ fails with network_error when PAR POST return }, } as unknown as ReturnType); - const result = yield* Micro.exit( + const result = yield* Effect.exit( createParAuthorizeUrlµ(wellknownWithPar, configWithPar, mockLog, mockStore), ); - expect(Micro.exitIsFailure(result)).toBe(true); - if (!Micro.exitIsFailure(result)) return; - expect(Micro.causeIsFail(result.cause)).toBe(true); - if (Micro.causeIsFail(result.cause)) { - expect(result.cause.error.type).toBe('network_error'); - } + expect(Exit.isFailure(result)).toBe(true); + if (!Exit.isFailure(result)) return; + const errorOpt = Cause.findErrorOption(result.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('network_error'); expect(sessionStorageStub.setItem).not.toHaveBeenCalled(); }), ); @@ -199,7 +196,7 @@ it.effect('createParAuthorizeUrlµ fails with network_error when PAR POST return it.effect( 'createParAuthorizeUrlµ fails with network_error when PAR response is missing request_uri', () => - Micro.gen(function* () { + Effect.gen(function* () { const configWithPar: OidcConfig = { ...config, par: true }; vi.stubGlobal('sessionStorage', sessionStorageStub); @@ -207,19 +204,18 @@ it.effect( data: {}, } as unknown as ReturnType); - const result = yield* Micro.exit( + const result = yield* Effect.exit( createParAuthorizeUrlµ(wellknownWithPar, configWithPar, mockLog, mockStore), ); - expect(Micro.exitIsFailure(result)).toBe(true); - if (!Micro.exitIsFailure(result)) return; - expect(Micro.causeIsFail(result.cause)).toBe(true); - if (Micro.causeIsFail(result.cause)) { - expect(result.cause.error.type).toBe('network_error'); - expect(result.cause.error.error_description).toBe( - "PAR response missing required 'request_uri' field", - ); - } + expect(Exit.isFailure(result)).toBe(true); + if (!Exit.isFailure(result)) return; + const errorOpt = Cause.findErrorOption(result.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('network_error'); + expect(errorOpt.value.error_description).toBe( + "PAR response missing required 'request_uri' field", + ); expect(sessionStorageStub.setItem).not.toHaveBeenCalled(); }), ); @@ -227,7 +223,7 @@ it.effect( it.effect( 'createParAuthorizeUrlµ with prompt=none includes prompt on slim authorize URL and in PAR body', () => - Micro.gen(function* () { + Effect.gen(function* () { const configWithPar: OidcConfig = { ...config, par: true }; const requestUri = 'urn:ietf:params:oauth:request_uri:prompt-none-test'; @@ -288,13 +284,15 @@ it('hasPushRequestUri returns false when request_uri is missing', () => { import { validateParResponseµ } from './authorize.request.micros.js'; it.effect('validateParResponseµ with SerializedError preserves error message', () => - Micro.gen(function* () { + Effect.gen(function* () { const serializedError = { name: 'Error', message: 'network timeout', code: 'FETCH_ERROR' }; - const exit = yield* Micro.exit(validateParResponseµ({ error: serializedError })); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return; + const exit = yield* Effect.exit(validateParResponseµ({ error: serializedError })); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; // Should surface the actual message, not generic "Unknown_Error" - expect(exit.cause.error.error_description).toContain('network timeout'); + expect(errorOpt.value.error_description).toContain('network timeout'); }), ); @@ -378,7 +376,7 @@ it('buildAuthorizeOptions falls back to "openid" scope and "code" responseType w // ─── authorizeµ flow routing ────────────────────────────────────────────────── it.effect('authorizeµ uses PAR flow when useParFlow=true', () => - Micro.gen(function* () { + Effect.gen(function* () { const requestUri = 'urn:ietf:params:oauth:request_uri:par-routing-test'; const authorizeResponse = { code: 'par-code', state: 'par-state' }; @@ -401,7 +399,7 @@ it.effect('authorizeµ uses PAR flow when useParFlow=true', () => ); it.effect('authorizeµ uses standard flow when useParFlow=false', () => - Micro.gen(function* () { + Effect.gen(function* () { const authorizeResponse = { code: 'std-code', state: 'std-state' }; vi.stubGlobal('sessionStorage', sessionStorageStub); @@ -422,7 +420,7 @@ it.effect('authorizeµ uses standard flow when useParFlow=false', () => it.effect( 'authorizeµ uses PAR flow when caller passes useParFlow=true (e.g. server requires PAR)', () => - Micro.gen(function* () { + Effect.gen(function* () { const requestUri = 'urn:ietf:params:oauth:request_uri:required-par-test'; const authorizeResponse = { code: 'req-par-code', state: 'req-par-state' }; @@ -452,7 +450,7 @@ it.effect( it.effect( 'authorizeµ routes to pi.flow fetch when options.responseMode is pi.flow (unwraps authorizeResponse)', () => - Micro.gen(function* () { + Effect.gen(function* () { // pi.flow dispatch goes through dispatchAuthorizeFetch which unwraps { authorizeResponse } const requestUri = 'urn:ietf:params:oauth:request_uri:pi-flow-test'; const authorizeResponse = { code: 'pi-code', state: 'pi-state' }; diff --git a/packages/oidc-client/src/lib/client.store.ts b/packages/oidc-client/src/lib/client.store.ts index dfc5624580..2e21580a61 100644 --- a/packages/oidc-client/src/lib/client.store.ts +++ b/packages/oidc-client/src/lib/client.store.ts @@ -7,13 +7,12 @@ import { logger as loggerFn } from '@forgerock/sdk-logger'; import { createAuthorizeUrl } from '@forgerock/sdk-oidc'; import { createStorage } from '@forgerock/storage'; -import { Micro } from 'effect'; -import { causeIsDie, exitIsFail, exitIsSuccess } from 'effect/Micro'; +import { Cause, Effect, Exit, Option } from 'effect'; import { authorizeµ, createParAuthorizeUrlµ } from './authorize.request.js'; import { buildTokenExchangeµ } from './exchange.request.js'; import { createClientStore, createTokenError, injectIntoStore } from './client.store.utils.js'; -import { handleMicroExit } from '@forgerock/sdk-utilities'; +import { handleExit } from '@forgerock/sdk-utilities'; import { isExpiryWithinThreshold } from './token.utils.js'; import { logoutµ } from './logout.request.js'; import { oidcApi } from './oidc.api.js'; @@ -151,34 +150,37 @@ export async function oidc( } if (useParFlow) { - const result = await Micro.runPromiseExit( + const result = await Effect.runPromiseExit( createParAuthorizeUrlµ(wellknown, config, log, store, options).pipe( - Micro.tapError((err) => - Micro.sync(() => + Effect.tapError((err) => + Effect.sync(() => log.error(`PAR authorize.url() failed [${err.type}]: ${err.error}`, err), ), ), ), ); - if (exitIsSuccess(result)) { + if (Exit.isSuccess(result)) { return result.value; - } else if (exitIsFail(result)) { - const authErr = result.cause.error; + } + const authUrlFailure = Cause.findErrorOption(result.cause); + if (Option.isSome(authUrlFailure)) { + const authErr = authUrlFailure.value; return { error: authErr.error, message: authErr.error_description, type: authErr.type, }; - } else { - const defect = causeIsDie(result.cause) ? result.cause.defect : undefined; - return { - error: 'PAR authorization failure', - message: - defect instanceof Error ? defect.message : String(defect ?? 'Unknown defect'), - type: 'auth_error', - }; } + const authUrlDefect = Cause.squash(result.cause); + return { + error: 'PAR authorization failure', + message: + authUrlDefect instanceof Error + ? authUrlDefect.message + : String(authUrlDefect ?? 'Unknown defect'), + type: 'auth_error', + }; } const optionsWithDefaults = { @@ -218,23 +220,26 @@ export async function oidc( }; } - const result = await Micro.runPromiseExit( + const result = await Effect.runPromiseExit( authorizeµ(wellknown, config, log, store, options, useParFlow), ); - if (exitIsSuccess(result)) { + if (Exit.isSuccess(result)) { return result.value; - } else if (exitIsFail(result)) { - return result.cause.error; - } else { - const defect = causeIsDie(result.cause) ? result.cause.defect : undefined; - return { - error: 'Authorization failure', - error_description: - defect instanceof Error ? defect.message : String(defect ?? 'Unknown defect'), - type: 'auth_error', - }; } + const bgAuthFailure = Cause.findErrorOption(result.cause); + if (Option.isSome(bgAuthFailure)) { + return bgAuthFailure.value; + } + const bgAuthDefect = Cause.squash(result.cause); + return { + error: 'Authorization failure', + error_description: + bgAuthDefect instanceof Error + ? bgAuthDefect.message + : String(bgAuthDefect ?? 'Unknown defect'), + type: 'auth_error', + }; }, }, /** @@ -273,14 +278,10 @@ export async function oidc( endpoint: wellknown.token_endpoint, store, options, - }).pipe( - Micro.tap(async (tokens) => { - await storageClient.set(tokens); - }), - ); + }).pipe(Effect.tap((tokens) => Effect.promise(() => storageClient.set(tokens)))); - const result = await Micro.runPromiseExit(getTokensµ); - return handleMicroExit(result, 'Token Exchange failure', 'exchange_error'); + const result = await Effect.runPromiseExit(getTokensµ); + return handleExit(result, 'Token Exchange failure', 'exchange_error'); }, /** @@ -342,47 +343,54 @@ export async function oidc( authorizeOptions, useParFlow, ).pipe( - Micro.flatMap((response): Micro.Micro => { - return buildTokenExchangeµ({ - code: response.code, - config, - log, - state: response.state, - endpoint: wellknown.token_endpoint, - store, - options: storageOptions, - }); - }), - Micro.tap(async (newTokens) => { - if (tokens && 'accessToken' in tokens) { - await store.dispatch( - oidcApi.endpoints.revoke.initiate({ - accessToken: tokens.accessToken, - clientId: config.clientId, - endpoint: wellknown.revocation_endpoint, - }), - ); - await storageClient.remove(); - } - await storageClient.set(newTokens); - }), + Effect.flatMap( + (response): Effect.Effect => { + return buildTokenExchangeµ({ + code: response.code, + config, + log, + state: response.state, + endpoint: wellknown.token_endpoint, + store, + options: storageOptions, + }); + }, + ), + Effect.tap((newTokens) => + Effect.promise(async () => { + if (tokens && 'accessToken' in tokens) { + await store.dispatch( + oidcApi.endpoints.revoke.initiate({ + accessToken: tokens.accessToken, + clientId: config.clientId, + endpoint: wellknown.revocation_endpoint, + }), + ); + await storageClient.remove(); + } + await storageClient.set(newTokens); + }), + ), ); - const result = await Micro.runPromiseExit(attemptAuthorizeGetTokensµ); + const result = await Effect.runPromiseExit(attemptAuthorizeGetTokensµ); - if (exitIsSuccess(result)) { + if (Exit.isSuccess(result)) { return result.value; - } else if (exitIsFail(result)) { - return result.cause.error; - } else { - const defect = causeIsDie(result.cause) ? result.cause.defect : undefined; - return { - error: 'Background token renewal failed', - error_description: - defect instanceof Error ? defect.message : String(defect ?? 'Unknown defect'), - type: 'auth_error', - }; } + const tokenGetFailure = Cause.findErrorOption(result.cause); + if (Option.isSome(tokenGetFailure)) { + return tokenGetFailure.value; + } + const tokenGetDefect = Cause.squash(result.cause); + return { + error: 'Background token renewal failed', + error_description: + tokenGetDefect instanceof Error + ? tokenGetDefect.message + : String(tokenGetDefect ?? 'Unknown defect'), + type: 'auth_error', + }; }, /** * @method revoke @@ -410,7 +418,7 @@ export async function oidc( }; } - const revokeµ = Micro.promise(() => + const revokeµ = Effect.promise(() => store.dispatch( oidcApi.endpoints.revoke.initiate({ accessToken: tokens.accessToken, @@ -419,7 +427,7 @@ export async function oidc( }), ), ).pipe( - Micro.map(({ error }) => { + Effect.map(({ error }) => { if (error) { let message = 'An error occurred while revoking the token'; let status: number | string = 'unknown'; @@ -440,9 +448,9 @@ export async function oidc( return null; }), // Delete local token and return combined results - Micro.flatMap((revokeResponse) => - Micro.promise(() => storageClient.remove()).pipe( - Micro.flatMap((deleteResponse) => { + Effect.flatMap((revokeResponse) => + Effect.promise(() => storageClient.remove()).pipe( + Effect.flatMap((deleteResponse) => { const isInnerRequestError = (revokeResponse && 'error' in revokeResponse) || (deleteResponse && 'error' in deleteResponse); @@ -453,21 +461,21 @@ export async function oidc( revokeResponse, deleteResponse, }; - return Micro.fail(result); + return Effect.fail(result); } else { const result: RevokeSuccessResult = { revokeResponse: null, deleteResponse: null, }; - return Micro.succeed(result); + return Effect.succeed(result); } }), ), ), ); - const result = await Micro.runPromiseExit(revokeµ); - return handleMicroExit(result, 'Token revocation failure', 'auth_error'); + const result = await Effect.runPromiseExit(revokeµ); + return handleExit(result, 'Token revocation failure', 'auth_error'); }, }, @@ -501,7 +509,7 @@ export async function oidc( }; } - const info = Micro.promise(() => + const info = Effect.promise(() => store.dispatch( oidcApi.endpoints.userInfo.initiate({ accessToken: tokens.accessToken, @@ -509,7 +517,7 @@ export async function oidc( }), ), ).pipe( - Micro.flatMap(({ data, error }) => { + Effect.flatMap(({ data, error }) => { if (error) { let message = 'An error occurred while fetching user info'; let status: number | string = 'unknown'; @@ -519,19 +527,19 @@ export async function oidc( if ('status' in error) { status = error.status; } - return Micro.fail({ + return Effect.fail({ error: 'User Info retrieval failure', message, type: 'auth_error', status, } as const); } - return Micro.succeed(data); + return Effect.succeed(data); }), ); - const result = await Micro.runPromiseExit(info); - return handleMicroExit(result, 'User Info retrieval failure', 'auth_error'); + const result = await Effect.runPromiseExit(info); + return handleExit(result, 'User Info retrieval failure', 'auth_error'); }, /** @@ -572,10 +580,10 @@ export async function oidc( return createTokenError('no_id_token'); } - const result = await Micro.runPromiseExit( + const result = await Effect.runPromiseExit( logoutµ({ tokens, config, wellknown, store, storageClient }), ); - return handleMicroExit(result, 'Logout_Failure', 'auth_error'); + return handleExit(result, 'Logout_Failure', 'auth_error'); }, /** @@ -600,13 +608,13 @@ export async function oidc( }; } - const micro = + const effect = options?.responseType === 'id_token' ? sessionCheckIdTokenµ(wellknown, config, store, storageClient, log, options) : sessionCheckNoneµ(wellknown, config, store, storageClient, log, options); - const result = await Micro.runPromiseExit(micro); - return handleMicroExit(result, 'Session check failure', 'unknown_error'); + const result = await Effect.runPromiseExit(effect); + return handleExit(result, 'Session check failure', 'unknown_error'); }, }, }; diff --git a/packages/oidc-client/src/lib/exchange.request.ts b/packages/oidc-client/src/lib/exchange.request.ts index 4fb4e57110..62b08a09f9 100644 --- a/packages/oidc-client/src/lib/exchange.request.ts +++ b/packages/oidc-client/src/lib/exchange.request.ts @@ -4,7 +4,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { Micro } from 'effect'; +import { Effect } from 'effect'; import { logger } from '@forgerock/sdk-logger'; @@ -34,22 +34,22 @@ export function buildTokenExchangeµ({ state, store, options, -}: BuildTokenExchangeµParams): Micro.Micro { +}: BuildTokenExchangeµParams): Effect.Effect { return createValuesµ(code, config, state, endpoint, options).pipe( - Micro.flatMap((options) => validateValuesµ(options)), - Micro.tap((options) => log.debug('Token exchange values created', options)), - Micro.tapError((options) => - Micro.sync(() => log.error('Error creating token exchange values', options)), + Effect.flatMap((options) => validateValuesµ(options)), + Effect.tap((options) => Effect.sync(() => log.debug('Token exchange values created', options))), + Effect.tapError((options) => + Effect.sync(() => log.error('Error creating token exchange values', options)), ), - Micro.flatMap((requestOptions) => - Micro.promise(() => store.dispatch(oidcApi.endpoints.exchange.initiate(requestOptions))), + Effect.flatMap((requestOptions) => + Effect.promise(() => store.dispatch(oidcApi.endpoints.exchange.initiate(requestOptions))), ), - Micro.flatMap(({ data, error }) => handleTokenResponseµ(data, error)), - Micro.tap((data) => log.debug('Token exchange response handled', data)), - Micro.tapError((error) => - Micro.sync(() => log.error('Error handling token exchange response', error)), + Effect.flatMap(({ data, error }) => handleTokenResponseµ(data, error)), + Effect.tap((data) => Effect.sync(() => log.debug('Token exchange response handled', data))), + Effect.tapError((error) => + Effect.sync(() => log.error('Error handling token exchange response', error)), ), - Micro.map((data) => { + Effect.map((data) => { const tokens = { accessToken: data.access_token, idToken: data.id_token, diff --git a/packages/oidc-client/src/lib/exchange.utils.test.ts b/packages/oidc-client/src/lib/exchange.utils.test.ts index d64cccabc8..454ac105ce 100644 --- a/packages/oidc-client/src/lib/exchange.utils.test.ts +++ b/packages/oidc-client/src/lib/exchange.utils.test.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ import { it, expect } from '@effect/vitest'; -import { Micro } from 'effect'; +import { Cause, Effect, Exit, Option } from 'effect'; import { handleTokenResponseµ, validateValuesµ } from './exchange.utils.js'; import type { OidcConfig } from './config.types.js'; import type { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; @@ -35,7 +35,7 @@ const storedValues: GetAuthorizationUrlOptions = { }; it.effect('validateValuesµ succeeds with TokenRequestOptions', () => - Micro.gen(function* () { + Effect.gen(function* () { const result = yield* validateValuesµ({ code, state, @@ -53,7 +53,7 @@ it.effect('validateValuesµ succeeds with TokenRequestOptions', () => ); it.effect('validateValuesµ with verifier succeeds with TokenRequestOptions', () => - Micro.gen(function* () { + Effect.gen(function* () { const verifier = 'verifier123'; const result = yield* validateValuesµ({ code, @@ -76,8 +76,8 @@ it.effect('validateValuesµ with verifier succeeds with TokenRequestOptions', () ); it.effect('validateValuesµ fails with state mismatch', () => - Micro.gen(function* () { - const result = yield* Micro.exit( + Effect.gen(function* () { + const result = yield* Effect.exit( validateValuesµ({ code, state: 'abcState', @@ -90,14 +90,17 @@ it.effect('validateValuesµ fails with state mismatch', () => }), ); - expect(result).toStrictEqual( - Micro.fail({ - error: 'State mismatch', - message: - 'The provided state does not match the stored state. This is likely due to passing in used, returned, authorize parameters.', - type: 'state_error', - }), - ); + expect(Exit.isFailure(result)).toBe(true); + if (!Exit.isFailure(result)) return; + const errorOpt = Cause.findErrorOption(result.cause); + expect(Option.isSome(errorOpt)).toBe(true); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value).toStrictEqual({ + error: 'State mismatch', + message: + 'The provided state does not match the stored state. This is likely due to passing in used, returned, authorize parameters.', + type: 'state_error', + }); }), ); @@ -107,7 +110,7 @@ it.effect('handleTokenResponseµ with data succeeds', () => { id_token: '67890', }; - return Micro.gen(function* () { + return Effect.gen(function* () { const result = yield* handleTokenResponseµ(data); expect(result).toStrictEqual(data); @@ -115,35 +118,41 @@ it.effect('handleTokenResponseµ with data succeeds', () => { }); it.effect('handleTokenResponseµ with no data fails', () => { - return Micro.gen(function* () { - const result = yield* Micro.exit(handleTokenResponseµ(undefined)); + return Effect.gen(function* () { + const result = yield* Effect.exit(handleTokenResponseµ(undefined)); - expect(result).toStrictEqual( - Micro.fail({ - error: 'Token Exchange failure', - message: 'No data returned from token exchange', - type: 'exchange_error', - }), - ); + expect(Exit.isFailure(result)).toBe(true); + if (!Exit.isFailure(result)) return; + const errorOpt = Cause.findErrorOption(result.cause); + expect(Option.isSome(errorOpt)).toBe(true); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value).toStrictEqual({ + error: 'Token Exchange failure', + message: 'No data returned from token exchange', + type: 'exchange_error', + }); }); }); it.effect('handleTokenResponseµ with error fails', () => { const errMessage = 'Fetch error message'; - return Micro.gen(function* () { - const result = yield* Micro.exit( + return Effect.gen(function* () { + const result = yield* Effect.exit( handleTokenResponseµ(undefined, { status: 'FETCH_ERROR', error: errMessage, }), ); - expect(result).toStrictEqual( - Micro.fail({ - error: 'Token Exchange failure', - message: errMessage, - type: 'exchange_error', - }), - ); + expect(Exit.isFailure(result)).toBe(true); + if (!Exit.isFailure(result)) return; + const errorOpt = Cause.findErrorOption(result.cause); + expect(Option.isSome(errorOpt)).toBe(true); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value).toStrictEqual({ + error: 'Token Exchange failure', + message: errMessage, + type: 'exchange_error', + }); }); }); diff --git a/packages/oidc-client/src/lib/exchange.utils.ts b/packages/oidc-client/src/lib/exchange.utils.ts index b3314a6ab4..30745c27a4 100644 --- a/packages/oidc-client/src/lib/exchange.utils.ts +++ b/packages/oidc-client/src/lib/exchange.utils.ts @@ -6,7 +6,7 @@ */ import type { SerializedError } from '@reduxjs/toolkit'; import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; -import { Micro } from 'effect'; +import { Effect } from 'effect'; import { getStoredAuthUrlValues } from '@forgerock/sdk-oidc'; import type { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; @@ -23,7 +23,7 @@ export function createValuesµ( endpoint: string, options?: Partial, ) { - return Micro.sync(() => { + return Effect.sync(() => { const storedValues = getStoredAuthUrlValues(config.clientId, options?.prefix); return { @@ -39,7 +39,7 @@ export function createValuesµ( export function handleTokenResponseµ( data: TokenExchangeResponse | undefined, error?: FetchBaseQueryError | SerializedError, -): Micro.Micro { +): Effect.Effect { if (error) { let message; if ('status' in error) { @@ -48,7 +48,7 @@ export function handleTokenResponseµ( message = error.message; } - return Micro.fail({ + return Effect.fail({ error: 'Token Exchange failure', message: message || 'Unknown error during token exchange', type: 'exchange_error', @@ -56,14 +56,14 @@ export function handleTokenResponseµ( } if (!data) { - return Micro.fail({ + return Effect.fail({ error: 'Token Exchange failure', message: 'No data returned from token exchange', type: 'exchange_error', } as TokenExchangeErrorResponse); } - return Micro.succeed(data); + return Effect.succeed(data); } export function validateValuesµ({ @@ -87,9 +87,9 @@ export function validateValuesµ({ type: 'state_error', } as const; - return Micro.fail(err); + return Effect.fail(err); } - return Micro.succeed({ + return Effect.succeed({ code, config, endpoint, diff --git a/packages/oidc-client/src/lib/logout.request.test.ts b/packages/oidc-client/src/lib/logout.request.test.ts index 1beaf712f4..81a662fb06 100644 --- a/packages/oidc-client/src/lib/logout.request.test.ts +++ b/packages/oidc-client/src/lib/logout.request.test.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ import { it, expect, describe } from 'vitest'; -import { Micro } from 'effect'; +import { Cause, Effect, Exit, Option } from 'effect'; import { deepStrictEqual } from 'node:assert'; import { setupServer } from 'msw/node'; import { http, HttpResponse } from 'msw'; @@ -101,8 +101,8 @@ const partialWellknown = { describe('signOutRedirectUri', () => { it('logoutµ appends post_logout_redirect_uri when signOutRedirectUri is set in config', () => - Micro.runPromise( - Micro.gen(function* () { + Effect.runPromise( + Effect.gen(function* () { const end_session_endpoint = 'https://example.com/am/oauth2/alpha/connect/endSession'; const revocation_endpoint = 'https://example.com/am/oauth2/alpha/token/revoke'; let capturedUrl = ''; @@ -132,8 +132,8 @@ describe('signOutRedirectUri', () => { )); it('logoutµ omits post_logout_redirect_uri when signOutRedirectUri is absent', () => - Micro.runPromise( - Micro.gen(function* () { + Effect.runPromise( + Effect.gen(function* () { const end_session_endpoint = 'https://example.com/am/oauth2/alpha/connect/endSession'; const revocation_endpoint = 'https://example.com/am/oauth2/alpha/token/revoke'; let capturedUrl = ''; @@ -165,8 +165,8 @@ describe('signOutRedirectUri', () => { describe('Ping AM', () => { it('logoutµ succeeds with valid wellknown endpoints', () => - Micro.runPromise( - Micro.gen(function* () { + Effect.runPromise( + Effect.gen(function* () { const end_session_endpoint = 'https://example.com/am/oauth2/alpha/connect/endSession'; const revocation_endpoint = 'https://example.com/am/oauth2/alpha/token/revoke'; @@ -191,12 +191,12 @@ describe('Ping AM', () => { )); it('logoutµ fails on bad endSession', () => - Micro.runPromise( - Micro.gen(function* () { + Effect.runPromise( + Effect.gen(function* () { const end_session_endpoint = 'https://example.com/am/oauth2/fake-realm/connect/endSession'; const revocation_endpoint = 'https://example.com/am/oauth2/alpha/token/revoke'; - const result = yield* Micro.exit( + const result = yield* Effect.exit( logoutµ({ tokens, config, @@ -210,30 +210,31 @@ describe('Ping AM', () => { }), ); - deepStrictEqual( - result, - Micro.exitFail({ - error: 'Inner request error', - sessionResponse: { - error: 'End Session failure', - message: 'An error occurred while ending the session', - type: 'auth_error', - status: 400, - }, - revokeResponse: null, - deleteResponse: null, - }), - ); + expect(Exit.isFailure(result)).toBe(true); + if (!Exit.isFailure(result)) return; + const errorOpt = Cause.findErrorOption(result.cause); + if (!Option.isSome(errorOpt)) return; + deepStrictEqual(errorOpt.value, { + error: 'Inner request error', + sessionResponse: { + error: 'End Session failure', + message: 'An error occurred while ending the session', + type: 'auth_error', + status: 400, + }, + revokeResponse: null, + deleteResponse: null, + }); }), )); it('logoutµ fails on bad revoke', () => - Micro.runPromise( - Micro.gen(function* () { + Effect.runPromise( + Effect.gen(function* () { const end_session_endpoint = 'https://example.com/am/oauth2/alpha/connect/endSession'; const revocation_endpoint = 'https://example.com/am/oauth2/fake-realm/token/revoke'; - const result = yield* Micro.exit( + const result = yield* Effect.exit( logoutµ({ tokens, config, @@ -247,20 +248,21 @@ describe('Ping AM', () => { }), ); - deepStrictEqual( - result, - Micro.exitFail({ - error: 'Inner request error', - sessionResponse: null, - revokeResponse: { - error: 'End Session failure', - message: 'An error occurred while ending the session', - type: 'auth_error', - status: 400, - }, - deleteResponse: null, - }), - ); + expect(Exit.isFailure(result)).toBe(true); + if (!Exit.isFailure(result)) return; + const errorOpt = Cause.findErrorOption(result.cause); + if (!Option.isSome(errorOpt)) return; + deepStrictEqual(errorOpt.value, { + error: 'Inner request error', + sessionResponse: null, + revokeResponse: { + error: 'End Session failure', + message: 'An error occurred while ending the session', + type: 'auth_error', + status: 400, + }, + deleteResponse: null, + }); }), )); }); @@ -269,8 +271,8 @@ describe('PingOne', () => { const fakeEndSessionEndpoint = 'https://example.com/endSession'; it('logoutµ succeeds with valid wellknown endpoints', () => - Micro.runPromise( - Micro.gen(function* () { + Effect.runPromise( + Effect.gen(function* () { const ping_end_idp_session_endpoint = 'https://example.com/as/idpSignoff'; const revocation_endpoint = 'https://example.com/as/revoke'; @@ -296,12 +298,12 @@ describe('PingOne', () => { )); it('logoutµ fails on bad endSession', () => - Micro.runPromise( - Micro.gen(function* () { + Effect.runPromise( + Effect.gen(function* () { const ping_end_idp_session_endpoint = 'https://example.com/as/badIdpSignoff'; const revocation_endpoint = 'https://example.com/as/revoke'; - const result = yield* Micro.exit( + const result = yield* Effect.exit( logoutµ({ tokens, config, @@ -316,30 +318,31 @@ describe('PingOne', () => { }), ); - deepStrictEqual( - result, - Micro.exitFail({ - error: 'Inner request error', - sessionResponse: { - error: 'End Session failure', - message: 'An error occurred while ending the session', - type: 'auth_error', - status: 400, - }, - revokeResponse: null, - deleteResponse: null, - }), - ); + expect(Exit.isFailure(result)).toBe(true); + if (!Exit.isFailure(result)) return; + const errorOpt = Cause.findErrorOption(result.cause); + if (!Option.isSome(errorOpt)) return; + deepStrictEqual(errorOpt.value, { + error: 'Inner request error', + sessionResponse: { + error: 'End Session failure', + message: 'An error occurred while ending the session', + type: 'auth_error', + status: 400, + }, + revokeResponse: null, + deleteResponse: null, + }); }), )); it('logoutµ fails on bad revoke', () => - Micro.runPromise( - Micro.gen(function* () { + Effect.runPromise( + Effect.gen(function* () { const ping_end_idp_session_endpoint = 'https://example.com/as/idpSignoff'; const revocation_endpoint = 'https://example.com/as/badRevoke'; - const result = yield* Micro.exit( + const result = yield* Effect.exit( logoutµ({ tokens, config, @@ -354,20 +357,21 @@ describe('PingOne', () => { }), ); - deepStrictEqual( - result, - Micro.exitFail({ - error: 'Inner request error', - sessionResponse: null, - revokeResponse: { - error: 'End Session failure', - message: 'An error occurred while ending the session', - type: 'auth_error', - status: 400, - }, - deleteResponse: null, - }), - ); + expect(Exit.isFailure(result)).toBe(true); + if (!Exit.isFailure(result)) return; + const errorOpt = Cause.findErrorOption(result.cause); + if (!Option.isSome(errorOpt)) return; + deepStrictEqual(errorOpt.value, { + error: 'Inner request error', + sessionResponse: null, + revokeResponse: { + error: 'End Session failure', + message: 'An error occurred while ending the session', + type: 'auth_error', + status: 400, + }, + deleteResponse: null, + }); }), )); }); diff --git a/packages/oidc-client/src/lib/logout.request.ts b/packages/oidc-client/src/lib/logout.request.ts index 636ab5fdd9..caf56f614d 100644 --- a/packages/oidc-client/src/lib/logout.request.ts +++ b/packages/oidc-client/src/lib/logout.request.ts @@ -4,7 +4,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { Micro } from 'effect'; +import { Effect } from 'effect'; import { oidcApi } from './oidc.api.js'; import { createLogoutError } from './client.store.utils.js'; @@ -26,9 +26,9 @@ export function logoutµ({ store: ClientStore; storageClient: StorageClient; }) { - return Micro.zip( + return Effect.zip( // End session with the ID token - Micro.promise(() => + Effect.promise(() => store.dispatch( oidcApi.endpoints.endSession.initiate({ idToken: tokens.idToken, @@ -36,10 +36,10 @@ export function logoutµ({ signOutRedirectUri: config.signOutRedirectUri, }), ), - ).pipe(Micro.map(({ data, error }) => createLogoutError(data, error))), + ).pipe(Effect.map(({ data, error }) => createLogoutError(data, error))), // Revoke the access token - Micro.promise(() => + Effect.promise(() => store.dispatch( oidcApi.endpoints.revoke.initiate({ accessToken: tokens.accessToken, @@ -47,12 +47,12 @@ export function logoutµ({ endpoint: wellknown.revocation_endpoint, }), ), - ).pipe(Micro.map(({ data, error }) => createLogoutError(data, error))), + ).pipe(Effect.map(({ data, error }) => createLogoutError(data, error))), ).pipe( // Delete local token and return combined results - Micro.flatMap(([sessionResponse, revokeResponse]) => - Micro.promise(() => storageClient.remove()).pipe( - Micro.flatMap((deleteResponse) => { + Effect.flatMap(([sessionResponse, revokeResponse]) => + Effect.promise(() => storageClient.remove()).pipe( + Effect.flatMap((deleteResponse) => { const isInnerRequestError = (sessionResponse && 'error' in sessionResponse) || (revokeResponse && 'error' in revokeResponse) || @@ -65,14 +65,14 @@ export function logoutµ({ revokeResponse, deleteResponse, }; - return Micro.fail(result); + return Effect.fail(result); } else { const result: LogoutSuccessResult = { sessionResponse: null, revokeResponse: null, deleteResponse: null, }; - return Micro.succeed(result); + return Effect.succeed(result); } }), ), diff --git a/packages/oidc-client/src/lib/session.micros.test.ts b/packages/oidc-client/src/lib/session.micros.test.ts index 581d84d366..3276a9624e 100644 --- a/packages/oidc-client/src/lib/session.micros.test.ts +++ b/packages/oidc-client/src/lib/session.micros.test.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ import { it, expect } from '@effect/vitest'; -import { Micro } from 'effect'; +import { Cause, Effect, Exit, Option } from 'effect'; import { vi, afterEach, describe } from 'vitest'; import * as sdkUtilities from '@forgerock/sdk-utilities'; @@ -181,32 +181,32 @@ describe('buildIdTokenUrl', () => { // ─── sessionCheckNoneµ (iframe path — with redirect_uri) ────────────────────── it.effect('sessionCheckNoneµ uses iframe path and succeeds when redirect_uri is configured', () => - Micro.gen(function* () { + Effect.gen(function* () { const { store, dispatch } = makeDispatchSetup({ data: { params: {} } }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( sessionCheckNoneµ(wellknown, config, store, makeStorageClient(storedTokens), log), ); - expect(Micro.exitIsSuccess(exit)).toBe(true); + expect(Exit.isSuccess(exit)).toBe(true); expect(dispatch).toHaveBeenCalledOnce(); }), ); it.effect('sessionCheckNoneµ iframe path fails with no_id_token_hint when storage is empty', () => - Micro.gen(function* () { + Effect.gen(function* () { const { store, dispatch } = makeDispatchSetup({ data: { params: {} } }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( sessionCheckNoneµ(wellknown, config, store, makeStorageClient(null), log), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('no_id_token_hint'); - expect(exit.cause.error.type).toBe('argument_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('no_id_token_hint'); + expect(errorOpt.value.type).toBe('argument_error'); expect(dispatch).not.toHaveBeenCalled(); }), ); @@ -214,11 +214,11 @@ it.effect('sessionCheckNoneµ iframe path fails with no_id_token_hint when stora // ─── sessionCheckNoneµ (fetch path — without redirect_uri) ─────────────────── it.effect('sessionCheckNoneµ uses fetch path and succeeds when no redirect_uri is configured', () => - Micro.gen(function* () { + Effect.gen(function* () { const configWithoutRedirectUri: OidcConfig = { ...config, redirectUri: '' }; const { store, dispatch } = makeFetchDispatchSetup({ data: { status: 204 } }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( sessionCheckNoneµ( wellknown, configWithoutRedirectUri, @@ -228,36 +228,34 @@ it.effect('sessionCheckNoneµ uses fetch path and succeeds when no redirect_uri ), ); - expect(Micro.exitIsSuccess(exit)).toBe(true); - if (!Micro.exitIsSuccess(exit)) { - return; - } + expect(Exit.isSuccess(exit)).toBe(true); + if (!Exit.isSuccess(exit)) return; expect(exit.value).toStrictEqual({ responseType: 'none' }); expect(dispatch).toHaveBeenCalledOnce(); }), ); it.effect('sessionCheckNoneµ fetch path fails with no_id_token_hint when storage is empty', () => - Micro.gen(function* () { + Effect.gen(function* () { const configWithoutRedirectUri: OidcConfig = { ...config, redirectUri: '' }; const { store, dispatch } = makeFetchDispatchSetup({ data: { status: 204 } }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( sessionCheckNoneµ(wellknown, configWithoutRedirectUri, store, makeStorageClient(null), log), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('no_id_token_hint'); - expect(exit.cause.error.type).toBe('argument_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('no_id_token_hint'); + expect(errorOpt.value.type).toBe('argument_error'); expect(dispatch).not.toHaveBeenCalled(); }), ); it.effect('sessionCheckNoneµ fetch path fails with login_required when AM returns 400', () => - Micro.gen(function* () { + Effect.gen(function* () { const configWithoutRedirectUri: OidcConfig = { ...config, redirectUri: '' }; const errorData: GenericError = { error: 'login_required', @@ -266,7 +264,7 @@ it.effect('sessionCheckNoneµ fetch path fails with login_required when AM retur }; const { store } = makeFetchDispatchSetup({ error: { data: errorData } }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( sessionCheckNoneµ( wellknown, configWithoutRedirectUri, @@ -276,26 +274,26 @@ it.effect('sessionCheckNoneµ fetch path fails with login_required when AM retur ), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('login_required'); - expect(exit.cause.error.type).toBe('auth_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('login_required'); + expect(errorOpt.value.type).toBe('auth_error'); }), ); // ─── dispatchSessionCheckFetchµ ─────────────────────────────────────────────── it.effect('dispatchSessionCheckFetchµ succeeds when dispatch resolves with data', () => - Micro.gen(function* () { + Effect.gen(function* () { const { store, dispatch } = makeFetchDispatchSetup({ data: { status: 204 } }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( dispatchSessionCheckFetchµ(store, 'https://example.com/authorize?prompt=none'), ); - expect(Micro.exitIsSuccess(exit)).toBe(true); + expect(Exit.isSuccess(exit)).toBe(true); expect(dispatch).toHaveBeenCalledOnce(); }), ); @@ -303,7 +301,7 @@ it.effect('dispatchSessionCheckFetchµ succeeds when dispatch resolves with data it.effect( 'dispatchSessionCheckFetchµ fails with auth_error when dispatch resolves with an error result', () => - Micro.gen(function* () { + Effect.gen(function* () { const errorData: GenericError = { error: 'login_required', message: 'The request requires login.', @@ -311,21 +309,21 @@ it.effect( }; const { store } = makeFetchDispatchSetup({ error: { data: errorData } }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( dispatchSessionCheckFetchµ(store, 'https://example.com/authorize?prompt=none'), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('login_required'); - expect(exit.cause.error.type).toBe('auth_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('login_required'); + expect(errorOpt.value.type).toBe('auth_error'); }), ); it.effect('dispatchSessionCheckFetchµ fails with network_error when dispatch rejects', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.spyOn(oidcApi.endpoints.sessionCheckFetch, 'initiate').mockReturnValue( Symbol('sentinel') as unknown as ReturnType< typeof oidcApi.endpoints.sessionCheckFetch.initiate @@ -334,23 +332,23 @@ it.effect('dispatchSessionCheckFetchµ fails with network_error when dispatch re const dispatch = vi.fn().mockRejectedValue(new Error('network failure')); const store: ClientStore = { dispatch } as unknown as ClientStore; - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( dispatchSessionCheckFetchµ(store, 'https://example.com/authorize?prompt=none'), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.type).toBe('network_error'); - expect(exit.cause.error.error).toBe('dispatch_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('network_error'); + expect(errorOpt.value.error).toBe('dispatch_error'); }), ); it.effect( 'dispatchSessionCheckFetchµ fails with network_error when queryFn receives a string-status error (e.g. FETCH_ERROR)', () => - Micro.gen(function* () { + Effect.gen(function* () { const errorData: GenericError = { error: 'session_check_error', message: 'A network error occurred during session check', @@ -358,23 +356,23 @@ it.effect( }; const { store } = makeFetchDispatchSetup({ error: { data: errorData } }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( dispatchSessionCheckFetchµ(store, 'https://example.com/authorize?prompt=none'), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('session_check_error'); - expect(exit.cause.error.type).toBe('network_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('session_check_error'); + expect(errorOpt.value.type).toBe('network_error'); }), ); it.effect( 'dispatchSessionCheckFetchµ fails with network_error when queryFn receives an unexpected 2xx status', () => - Micro.gen(function* () { + Effect.gen(function* () { const errorData: GenericError = { error: 'session_check_error', message: 'Unexpected response status: 200', @@ -382,23 +380,23 @@ it.effect( }; const { store } = makeFetchDispatchSetup({ error: { data: errorData } }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( dispatchSessionCheckFetchµ(store, 'https://example.com/authorize?prompt=none'), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('session_check_error'); - expect(exit.cause.error.type).toBe('network_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('session_check_error'); + expect(errorOpt.value.type).toBe('network_error'); }), ); // ─── sessionCheckIdTokenµ ───────────────────────────────────────────────────── it.effect('sessionCheckIdTokenµ returns claims on valid JWT', () => - Micro.gen(function* () { + Effect.gen(function* () { const knownNonce = 'test-nonce-value-12345678901234'; const knownState = 'known-state-value'; vi.spyOn(sdkUtilities, 'createRandomString').mockReturnValue(knownNonce); @@ -415,72 +413,68 @@ it.effect('sessionCheckIdTokenµ returns claims on valid JWT', () => data: { params: { id_token: validJwt, state: knownState } }, }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( sessionCheckIdTokenµ(wellknown, config, store, makeStorageClient(storedTokens), log), ); - expect(Micro.exitIsSuccess(exit)).toBe(true); - if (!Micro.exitIsSuccess(exit)) { - return; - } + expect(Exit.isSuccess(exit)).toBe(true); + if (!Exit.isSuccess(exit)) return; expect(exit.value.responseType).toBe('id_token'); expect(exit.value.claims).toBeDefined(); - if (!exit.value.claims) { - return; - } + if (!exit.value.claims) return; expect(exit.value.claims['nonce']).toBe(knownNonce); }), ); it.effect('sessionCheckIdTokenµ fails with state_mismatch when response state does not match', () => - Micro.gen(function* () { + Effect.gen(function* () { vi.spyOn(sdkUtilities, 'createState').mockReturnValue('known-state-value'); const { store } = makeDispatchSetup({ data: { params: { id_token: 'some.jwt.token', state: 'tampered-state' } }, }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( sessionCheckIdTokenµ(wellknown, config, store, makeStorageClient(storedTokens), log), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('state_mismatch'); - expect(exit.cause.error.type).toBe('auth_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('state_mismatch'); + expect(errorOpt.value.type).toBe('auth_error'); }), ); it.effect('sessionCheckIdTokenµ fails with no_id_token when iframe returns no id_token param', () => - Micro.gen(function* () { + Effect.gen(function* () { const knownState = 'known-state-value'; vi.spyOn(sdkUtilities, 'createState').mockReturnValue(knownState); const { store } = makeDispatchSetup({ data: { params: { state: knownState } } }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( sessionCheckIdTokenµ(wellknown, config, store, makeStorageClient(storedTokens), log), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('no_id_token'); - expect(exit.cause.error.type).toBe('auth_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('no_id_token'); + expect(errorOpt.value.type).toBe('auth_error'); }), ); it.effect( 'sessionCheckIdTokenµ fails with missing_redirect_uri when no redirect_uri is configured', () => - Micro.gen(function* () { + Effect.gen(function* () { const configWithoutRedirectUri: OidcConfig = { ...config, redirectUri: '' }; const { store, dispatch } = makeDispatchSetup({ data: { params: {} } }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( sessionCheckIdTokenµ( wellknown, configWithoutRedirectUri, @@ -490,12 +484,12 @@ it.effect( ), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('missing_redirect_uri'); - expect(exit.cause.error.type).toBe('argument_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('missing_redirect_uri'); + expect(errorOpt.value.type).toBe('argument_error'); expect(dispatch).not.toHaveBeenCalled(); }), ); @@ -503,33 +497,33 @@ it.effect( // ─── readStoredIdTokenµ ─────────────────────────────────────────────────────── it.effect('readStoredIdTokenµ returns idToken string when tokens are stored', () => - Micro.gen(function* () { + Effect.gen(function* () { const result = yield* readStoredIdTokenµ(makeStorageClient(storedTokens)); expect(result).toBe(storedTokens.idToken); }), ); it.effect('readStoredIdTokenµ returns null when storage is empty', () => - Micro.gen(function* () { + Effect.gen(function* () { const result = yield* readStoredIdTokenµ(makeStorageClient(null)); expect(result).toBeNull(); }), ); it.effect('readStoredIdTokenµ fails with argument_error when storageClient.get rejects', () => - Micro.gen(function* () { + Effect.gen(function* () { const failingStorage: StorageClient = { get: vi.fn().mockRejectedValue(new Error('storage unavailable')), set: vi.fn(), remove: vi.fn(), }; - const exit = yield* Micro.exit(readStoredIdTokenµ(failingStorage)); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.type).toBe('argument_error'); - expect(exit.cause.error.error).toBe('storage_error'); + const exit = yield* Effect.exit(readStoredIdTokenµ(failingStorage)); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('argument_error'); + expect(errorOpt.value.error).toBe('storage_error'); }), ); @@ -538,7 +532,7 @@ it.effect('readStoredIdTokenµ fails with argument_error when storageClient.get it.effect( 'dispatchSessionCheckIframeµ succeeds and returns params when dispatch resolves with data', () => - Micro.gen(function* () { + Effect.gen(function* () { const params = { state: 'ok' }; const dispatch = vi.fn().mockResolvedValue({ data: { params } }); const store: ClientStore = { dispatch } as unknown as ClientStore; @@ -560,7 +554,7 @@ it.effect( it.effect( 'dispatchSessionCheckIframeµ fails with auth_error when dispatch resolves with an error result', () => - Micro.gen(function* () { + Effect.gen(function* () { const errorData: GenericError = { error: 'login_required', message: 'User must authenticate', @@ -574,20 +568,20 @@ it.effect( >, ); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( dispatchSessionCheckIframeµ(store, 'https://example.com/authorize?prompt=none', 'none'), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('login_required'); - expect(exit.cause.error.type).toBe('auth_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('login_required'); + expect(errorOpt.value.type).toBe('auth_error'); }), ); it.effect('dispatchSessionCheckIframeµ fails with network_error when dispatch rejects', () => - Micro.gen(function* () { + Effect.gen(function* () { const dispatch = vi.fn().mockRejectedValue(new Error('network failure')); const store: ClientStore = { dispatch } as unknown as ClientStore; vi.spyOn(oidcApi.endpoints.sessionCheckIframe, 'initiate').mockReturnValue( @@ -596,15 +590,15 @@ it.effect('dispatchSessionCheckIframeµ fails with network_error when dispatch r >, ); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( dispatchSessionCheckIframeµ(store, 'https://example.com/authorize?prompt=none', 'none'), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.type).toBe('network_error'); - expect(exit.cause.error.error).toBe('dispatch_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.type).toBe('network_error'); + expect(errorOpt.value.error).toBe('dispatch_error'); }), ); @@ -621,7 +615,7 @@ function makeJwtWithClaims(claims: JWTPayload): string { it.effect( 'validateSessionCheckResponseµ succeeds and returns claims when state and nonce match', () => - Micro.gen(function* () { + Effect.gen(function* () { const nonce = 'expected-nonce'; const state = 'expected-state'; const jwt = makeJwtWithClaims({ nonce, sub: 'user1' }); @@ -631,7 +625,7 @@ it.effect( ); it.effect('validateSessionCheckResponseµ succeeds when state, nonce, and subject all match', () => - Micro.gen(function* () { + Effect.gen(function* () { const nonce = 'nonce-abc'; const state = 'state-abc'; const jwt = makeJwtWithClaims({ nonce, sub: 'user1' }); @@ -646,81 +640,81 @@ it.effect('validateSessionCheckResponseµ succeeds when state, nonce, and subjec ); it.effect('validateSessionCheckResponseµ fails with state_mismatch when state does not match', () => - Micro.gen(function* () { + Effect.gen(function* () { const jwt = makeJwtWithClaims({ nonce: 'nonce', sub: 'user1' }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( validateSessionCheckResponseµ( { id_token: jwt, state: 'tampered' }, 'expected-state', 'nonce', ), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('state_mismatch'); - expect(exit.cause.error.type).toBe('auth_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('state_mismatch'); + expect(errorOpt.value.type).toBe('auth_error'); }), ); it.effect('validateSessionCheckResponseµ fails with no_id_token when id_token is absent', () => - Micro.gen(function* () { + Effect.gen(function* () { const state = 'expected-state'; - const exit = yield* Micro.exit(validateSessionCheckResponseµ({ state }, state, 'nonce')); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('no_id_token'); - expect(exit.cause.error.type).toBe('auth_error'); + const exit = yield* Effect.exit(validateSessionCheckResponseµ({ state }, state, 'nonce')); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('no_id_token'); + expect(errorOpt.value.type).toBe('auth_error'); }), ); it.effect('validateSessionCheckResponseµ fails with nonce_mismatch when nonce does not match', () => - Micro.gen(function* () { + Effect.gen(function* () { const state = 'expected-state'; const jwt = makeJwtWithClaims({ nonce: 'wrong-nonce', sub: 'user1' }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( validateSessionCheckResponseµ({ id_token: jwt, state }, state, 'expected-nonce'), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('nonce_mismatch'); - expect(exit.cause.error.type).toBe('auth_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('nonce_mismatch'); + expect(errorOpt.value.type).toBe('auth_error'); }), ); it.effect('validateSessionCheckResponseµ fails with subject_mismatch when sub does not match', () => - Micro.gen(function* () { + Effect.gen(function* () { const nonce = 'valid-nonce'; const state = 'expected-state'; const jwt = makeJwtWithClaims({ nonce, sub: 'user2' }); - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( validateSessionCheckResponseµ({ id_token: jwt, state }, state, nonce, 'user1'), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('subject_mismatch'); - expect(exit.cause.error.type).toBe('auth_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('subject_mismatch'); + expect(errorOpt.value.type).toBe('auth_error'); }), ); it.effect('validateSessionCheckResponseµ fails with invalid_jwt when JWT is malformed', () => - Micro.gen(function* () { + Effect.gen(function* () { const state = 'expected-state'; - const exit = yield* Micro.exit( + const exit = yield* Effect.exit( validateSessionCheckResponseµ({ id_token: 'not.a.valid.jwt.payload', state }, state, 'nonce'), ); - expect(Micro.exitIsFailure(exit)).toBe(true); - if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) { - return; - } - expect(exit.cause.error.error).toBe('invalid_jwt'); - expect(exit.cause.error.type).toBe('auth_error'); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + const errorOpt = Cause.findErrorOption(exit.cause); + if (!Option.isSome(errorOpt)) return; + expect(errorOpt.value.error).toBe('invalid_jwt'); + expect(errorOpt.value.type).toBe('auth_error'); }), ); diff --git a/packages/oidc-client/src/lib/session.micros.ts b/packages/oidc-client/src/lib/session.micros.ts index 561291dce0..650c0a03ad 100644 --- a/packages/oidc-client/src/lib/session.micros.ts +++ b/packages/oidc-client/src/lib/session.micros.ts @@ -4,7 +4,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { Micro } from 'effect'; +import { Effect } from 'effect'; import { createRandomString, createState } from '@forgerock/sdk-utilities'; @@ -24,15 +24,15 @@ import type { SessionCheckOptions, SessionCheckSuccess } from './session.types.j export const readStoredIdTokenµ = ( storageClient: StorageClient, -): Micro.Micro => - Micro.tryPromise({ +): Effect.Effect => + Effect.tryPromise({ try: () => storageClient.get(), catch: (): GenericError => ({ error: 'storage_error', message: 'Failed to read tokens from storage', type: 'argument_error', }), - }).pipe(Micro.map((tokens) => (tokens && 'idToken' in tokens ? tokens.idToken : null))); + }).pipe(Effect.map((tokens) => (tokens && 'idToken' in tokens ? tokens.idToken : null))); // ─── Dispatch ──────────────────────────────────────────────────────────────── @@ -40,8 +40,8 @@ export const dispatchSessionCheckIframeµ = ( store: ClientStore, url: string, responseType: 'id_token' | 'none', -): Micro.Micro, GenericError, never> => - Micro.tryPromise({ +): Effect.Effect, GenericError, never> => + Effect.tryPromise({ try: () => store.dispatch(oidcApi.endpoints.sessionCheckIframe.initiate({ url, responseType })), catch: (err): GenericError => ({ error: 'dispatch_error', @@ -49,27 +49,27 @@ export const dispatchSessionCheckIframeµ = ( type: 'network_error', }), }).pipe( - Micro.flatMap((result) => { + Effect.flatMap((result) => { if ('error' in result && result.error) { const errData = result.error as { data?: { error?: string; message?: string; type?: string }; }; - return Micro.fail({ + return Effect.fail({ error: errData.data?.error ?? 'session_check_error', message: errData.data?.message ?? 'An error occurred during session check', type: (errData.data?.type as GenericError['type']) ?? 'network_error', }); } const { params } = (result as { data: { params: Record } }).data; - return Micro.succeed(params); + return Effect.succeed(params); }), ); export const dispatchSessionCheckFetchµ = ( store: ClientStore, url: string, -): Micro.Micro => - Micro.tryPromise({ +): Effect.Effect => + Effect.tryPromise({ try: () => store.dispatch(oidcApi.endpoints.sessionCheckFetch.initiate({ url })), catch: (err): GenericError => ({ error: 'dispatch_error', @@ -77,18 +77,18 @@ export const dispatchSessionCheckFetchµ = ( type: 'network_error', }), }).pipe( - Micro.flatMap((result) => { + Effect.flatMap((result) => { if ('error' in result && result.error) { const errData = result.error as { data?: { error?: string; message?: string; type?: string }; }; - return Micro.fail({ + return Effect.fail({ error: errData.data?.error ?? 'login_required', message: errData.data?.message ?? 'The request requires login.', type: (errData.data?.type as GenericError['type']) ?? 'auth_error', }); } - return Micro.void; + return Effect.void; }), ); @@ -99,10 +99,10 @@ export const validateSessionCheckResponseµ = ( state: string, nonce: string, subject?: string, -): Micro.Micro => { - return Micro.gen(function* () { +): Effect.Effect => { + return Effect.gen(function* () { if (iframeParams.state !== state) { - return yield* Micro.fail({ + return yield* Effect.fail({ error: 'state_mismatch', message: 'State parameter in response does not match the expected value', type: 'auth_error', @@ -111,14 +111,14 @@ export const validateSessionCheckResponseµ = ( const idToken = iframeParams.id_token; if (!idToken) { - return yield* Micro.fail({ + return yield* Effect.fail({ error: 'no_id_token', message: 'No id_token found in iframe response', type: 'auth_error', }); } - const claims = yield* Micro.try({ + const claims = yield* Effect.try({ try: () => decodeJwt(idToken), catch: (): GenericError => ({ error: 'invalid_jwt', @@ -128,7 +128,7 @@ export const validateSessionCheckResponseµ = ( }); if (claims.nonce !== nonce) { - return yield* Micro.fail({ + return yield* Effect.fail({ error: 'nonce_mismatch', message: 'Nonce in id_token does not match the expected value', type: 'auth_error', @@ -136,7 +136,7 @@ export const validateSessionCheckResponseµ = ( } if (subject !== undefined && claims.sub !== subject) { - return yield* Micro.fail({ + return yield* Effect.fail({ error: 'subject_mismatch', message: 'Subject claim in id_token does not match the expected value', type: 'auth_error', @@ -198,11 +198,11 @@ export const sessionCheckNoneµ = ( storageClient: StorageClient, log: CustomLogger, options?: SessionCheckOptions, -): Micro.Micro => { +): Effect.Effect => { return readStoredIdTokenµ(storageClient).pipe( - Micro.flatMap((storedIdToken) => { + Effect.flatMap((storedIdToken) => { if (!storedIdToken) { - return Micro.fail({ + return Effect.fail({ error: 'no_id_token_hint', message: 'response_type=none requires a stored id_token; authenticate first', type: 'argument_error', @@ -224,8 +224,8 @@ export const sessionCheckNoneµ = ( ? dispatchSessionCheckIframeµ(store, url, 'none') : dispatchSessionCheckFetchµ(store, url); }), - Micro.tap(() => log.debug('Session check (none) completed successfully')), - Micro.map((): SessionCheckSuccess => ({ responseType: 'none' })), + Effect.tap(() => Effect.sync(() => log.debug('Session check (none) completed successfully'))), + Effect.map((): SessionCheckSuccess => ({ responseType: 'none' })), ); }; @@ -238,11 +238,11 @@ export const sessionCheckIdTokenµ = ( storageClient: StorageClient, log: CustomLogger, options?: SessionCheckOptions, -): Micro.Micro => { +): Effect.Effect => { const redirectUri = options?.redirectUri ?? config.redirectUri; if (!redirectUri) { - return Micro.fail({ + return Effect.fail({ error: 'missing_redirect_uri', message: 'redirect_uri is required for session check with response_type=id_token', type: 'argument_error', @@ -250,7 +250,7 @@ export const sessionCheckIdTokenµ = ( } return readStoredIdTokenµ(storageClient).pipe( - Micro.flatMap((storedIdToken) => { + Effect.flatMap((storedIdToken) => { const { url, nonce, state } = buildIdTokenUrl( wellknown.authorization_endpoint, config, @@ -259,12 +259,14 @@ export const sessionCheckIdTokenµ = ( options, ); return dispatchSessionCheckIframeµ(store, url, 'id_token').pipe( - Micro.flatMap((iframeParams) => + Effect.flatMap((iframeParams) => validateSessionCheckResponseµ(iframeParams, state, nonce, options?.subject), ), ); }), - Micro.tap(() => log.debug('Session check (id_token) completed successfully')), - Micro.map((claims): SessionCheckSuccess => ({ responseType: 'id_token', claims })), + Effect.tap(() => + Effect.sync(() => log.debug('Session check (id_token) completed successfully')), + ), + Effect.map((claims): SessionCheckSuccess => ({ responseType: 'id_token', claims })), ); }; diff --git a/packages/sdk-utilities/src/lib/config/config.effects.ts b/packages/sdk-utilities/src/lib/config/config.effects.ts index 76defcf49a..c9a05a8908 100644 --- a/packages/sdk-utilities/src/lib/config/config.effects.ts +++ b/packages/sdk-utilities/src/lib/config/config.effects.ts @@ -5,23 +5,23 @@ * of the MIT license. See the LICENSE file for details. */ -import * as Either from 'effect/Either'; +import { Result } from 'effect'; import { parseToOidcConfig, parseToJourneyConfig, parseToDavinciConfig } from './config.utils.js'; import type { OidcConfig, JourneyClientConfig, DaVinciConfig } from './config.types.js'; -function throwOnLeft(result: Either.Either): T { - if (Either.isLeft(result)) { - const messages = result.left.map((e) => `${e.field}: ${e.message}`).join(', '); +function throwOnFail(result: Result.Result): T { + if (Result.isFailure(result)) { + const messages = result.failure.map((e) => `${e.field}: ${e.message}`).join(', '); throw new Error(`Invalid unified SDK config: ${messages}`); } - return result.right; + return result.success; } -export const makeOidcConfig = (json: unknown): OidcConfig => throwOnLeft(parseToOidcConfig(json)); +export const makeOidcConfig = (json: unknown): OidcConfig => throwOnFail(parseToOidcConfig(json)); export const makeJourneyConfig = (json: unknown): JourneyClientConfig => - throwOnLeft(parseToJourneyConfig(json)); + throwOnFail(parseToJourneyConfig(json)); export const makeDavinciConfig = (json: unknown): DaVinciConfig => - throwOnLeft(parseToDavinciConfig(json)); + throwOnFail(parseToDavinciConfig(json)); diff --git a/packages/sdk-utilities/src/lib/config/config.test.ts b/packages/sdk-utilities/src/lib/config/config.test.ts index a48407f9d8..c981091ea8 100644 --- a/packages/sdk-utilities/src/lib/config/config.test.ts +++ b/packages/sdk-utilities/src/lib/config/config.test.ts @@ -6,7 +6,7 @@ */ import { describe, it, expect } from 'vitest'; -import * as Either from 'effect/Either'; +import { Result } from 'effect'; import { parseToOidcConfig, parseToJourneyConfig, @@ -59,31 +59,31 @@ const journeyOnlyConfig = { describe('parseUnifiedSdkConfig', () => { it('parseUnifiedSdkConfig_ValidFullConfig_ReturnsSuccess', () => { - expect(Either.isRight(parseUnifiedSdkConfig(fullConfig))).toBe(true); + expect(Result.isSuccess(parseUnifiedSdkConfig(fullConfig))).toBe(true); }); it('parseUnifiedSdkConfig_JourneyOnlyConfig_ReturnsSuccess', () => { - expect(Either.isRight(parseUnifiedSdkConfig(journeyOnlyConfig))).toBe(true); + expect(Result.isSuccess(parseUnifiedSdkConfig(journeyOnlyConfig))).toBe(true); }); it('parseUnifiedSdkConfig_NoOidcOrJourneySection_ReturnsSuccess', () => { - expect(Either.isRight(parseUnifiedSdkConfig({ timeout: 5000 }))).toBe(true); + expect(Result.isSuccess(parseUnifiedSdkConfig({ timeout: 5000 }))).toBe(true); }); it('parseUnifiedSdkConfig_UnknownTopLevelField_Ignored', () => { - expect(Either.isRight(parseUnifiedSdkConfig({ timeout: 5000, surprise: 'kept' }))).toBe(true); + expect(Result.isSuccess(parseUnifiedSdkConfig({ timeout: 5000, surprise: 'kept' }))).toBe(true); }); it('parseUnifiedSdkConfig_TimeoutNotNumber_ReturnsTypeError', () => { - const errors = Either.getOrThrow( - Either.flip(parseUnifiedSdkConfig({ ...fullConfig, timeout: 'thirty' })), + const errors = Result.getOrThrow( + Result.flip(parseUnifiedSdkConfig({ ...fullConfig, timeout: 'thirty' })), ); expect(errors.some((e) => e.field === 'timeout')).toBe(true); }); it('parseUnifiedSdkConfig_JourneyMissingServerUrl_ReturnsError', () => { - const errors = Either.getOrThrow( - Either.flip( + const errors = Result.getOrThrow( + Result.flip( parseUnifiedSdkConfig({ journey: { realm: 'alpha' }, oidc: { discoveryEndpoint: 'https://example.com/.well-known/openid-configuration' }, @@ -94,15 +94,15 @@ describe('parseUnifiedSdkConfig', () => { }); it('parseUnifiedSdkConfig_InvalidOidcNested_PropagatesErrors', () => { - const errors = Either.getOrThrow( - Either.flip(parseUnifiedSdkConfig({ ...fullConfig, oidc: { ...minimalOidc, clientId: 42 } })), + const errors = Result.getOrThrow( + Result.flip(parseUnifiedSdkConfig({ ...fullConfig, oidc: { ...minimalOidc, clientId: 42 } })), ); expect(errors.some((e) => e.field === 'oidc.clientId')).toBe(true); }); it('parseUnifiedSdkConfig_MultipleErrors_AllAccumulated', () => { - const errors = Either.getOrThrow( - Either.flip(parseUnifiedSdkConfig({ timeout: 'thirty', oidc: { scopes: 'not-an-array' } })), + const errors = Result.getOrThrow( + Result.flip(parseUnifiedSdkConfig({ timeout: 'thirty', oidc: { scopes: 'not-an-array' } })), ); expect(errors.length).toBeGreaterThanOrEqual(2); expect(errors.some((e) => e.field === 'timeout')).toBe(true); @@ -112,14 +112,14 @@ describe('parseUnifiedSdkConfig', () => { describe('parseToOidcConfig', () => { it('parseToOidcConfig_NoOidcBlock_ReturnsFailure', () => { - const errors = Either.getOrThrow( - Either.flip(parseToOidcConfig({ journey: { serverUrl: 'https://example.com/am' } })), + const errors = Result.getOrThrow( + Result.flip(parseToOidcConfig({ journey: { serverUrl: 'https://example.com/am' } })), ); expect(errors.some((e) => e.field === 'oidc')).toBe(true); }); it('parseToOidcConfig_MinimalConfig_MapsRequiredFields', () => { - const data = Either.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })); + const data = Result.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })); expect(data.clientId).toBe('my-client'); expect(data.redirectUri).toBe('https://app.example.com/callback'); expect(data.scope).toBe('openid profile'); @@ -129,14 +129,14 @@ describe('parseToOidcConfig', () => { }); it('parseToOidcConfig_ScopesJoinedWithSpace', () => { - const data = Either.getOrThrow( + const data = Result.getOrThrow( parseToOidcConfig({ oidc: { ...minimalOidc, scopes: ['openid', 'email'] } }), ); expect(data.scope).toBe('openid email'); }); it('parseToOidcConfig_RefreshThresholdConvertedToMs', () => { - const data = Either.getOrThrow( + const data = Result.getOrThrow( parseToOidcConfig({ oidc: { ...minimalOidc, refreshThreshold: 60 } }), ); expect(data.oauthThreshold).toBe(60000); @@ -144,12 +144,12 @@ describe('parseToOidcConfig', () => { it('parseToOidcConfig_NoRefreshThreshold_OauthThresholdAbsent', () => { expect( - Either.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })).oauthThreshold, + Result.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })).oauthThreshold, ).toBeUndefined(); }); it('parseToOidcConfig_RealmMappedToRealmPath', () => { - const data = Either.getOrThrow( + const data = Result.getOrThrow( parseToOidcConfig({ journey: { serverUrl: 'https://example.com/am', realm: 'alpha' }, oidc: minimalOidc, @@ -159,22 +159,22 @@ describe('parseToOidcConfig', () => { }); it('parseToOidcConfig_NoRealm_RealmPathAbsent', () => { - expect(Either.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })).realmPath).toBeUndefined(); + expect(Result.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })).realmPath).toBeUndefined(); }); it('parseToOidcConfig_TimeoutPassedToServerConfig', () => { - const data = Either.getOrThrow(parseToOidcConfig({ timeout: 5000, oidc: minimalOidc })); + const data = Result.getOrThrow(parseToOidcConfig({ timeout: 5000, oidc: minimalOidc })); expect(data.serverConfig.timeout).toBe(5000); }); it('parseToOidcConfig_NoTimeout_TimeoutAbsentInServerConfig', () => { expect( - Either.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })).serverConfig.timeout, + Result.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })).serverConfig.timeout, ).toBeUndefined(); }); it('parseToOidcConfig_AuthorizeParamsMapped', () => { - const data = Either.getOrThrow( + const data = Result.getOrThrow( parseToOidcConfig({ oidc: { ...minimalOidc, @@ -198,22 +198,22 @@ describe('parseToOidcConfig', () => { }); it('parseToOidcConfig_EmptyScopes_ReturnsFailure', () => { - const errors = Either.getOrThrow( - Either.flip(parseToOidcConfig({ oidc: { ...minimalOidc, scopes: [] } })), + const errors = Result.getOrThrow( + Result.flip(parseToOidcConfig({ oidc: { ...minimalOidc, scopes: [] } })), ); expect(errors.some((e) => e.field === 'oidc.scopes')).toBe(true); }); it('parseToOidcConfig_NoAuthorizeParams_AllAbsent', () => { - const data = Either.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })); + const data = Result.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })); expect(data.loginHint).toBeUndefined(); expect(data.nonce).toBeUndefined(); expect(data.query).toBeUndefined(); }); it('parseToOidcConfig_OidcMissingDiscoveryEndpoint_ReturnsFailure', () => { - const errors = Either.getOrThrow( - Either.flip( + const errors = Result.getOrThrow( + Result.flip( parseToOidcConfig({ oidc: { clientId: 'x', redirectUri: 'x', scopes: ['openid'] } }), ), ); @@ -221,34 +221,34 @@ describe('parseToOidcConfig', () => { }); it('parseToOidcConfig_NullInput_ReturnsFailure', () => { - expect(Either.isLeft(parseToOidcConfig(null))).toBe(true); + expect(Result.isFailure(parseToOidcConfig(null))).toBe(true); }); }); describe('parseToJourneyConfig', () => { it('parseToJourneyConfig_NoOidcBlock_ReturnsFailure', () => { - const errors = Either.getOrThrow( - Either.flip(parseToJourneyConfig({ journey: { serverUrl: 'https://example.com/am' } })), + const errors = Result.getOrThrow( + Result.flip(parseToJourneyConfig({ journey: { serverUrl: 'https://example.com/am' } })), ); expect(errors.some((e) => e.field === 'oidc')).toBe(true); }); it('parseToJourneyConfig_MinimalConfig_MapsWellknown', () => { - const data = Either.getOrThrow(parseToJourneyConfig({ oidc: minimalOidc })); + const data = Result.getOrThrow(parseToJourneyConfig({ oidc: minimalOidc })); expect(data.serverConfig.wellknown).toBe( 'https://example.com/.well-known/openid-configuration', ); }); it('parseToJourneyConfig_JourneyOnlyConfig_MapsWellknown', () => { - const data = Either.getOrThrow(parseToJourneyConfig(journeyOnlyConfig)); + const data = Result.getOrThrow(parseToJourneyConfig(journeyOnlyConfig)); expect(data.serverConfig.wellknown).toBe( 'https://example.com/.well-known/openid-configuration', ); }); it('parseToJourneyConfig_RealmMappedToRealmPath', () => { - const data = Either.getOrThrow( + const data = Result.getOrThrow( parseToJourneyConfig({ journey: { serverUrl: 'https://example.com/am', realm: 'beta' }, oidc: minimalOidc, @@ -259,17 +259,17 @@ describe('parseToJourneyConfig', () => { it('parseToJourneyConfig_NoRealm_RealmPathAbsent', () => { expect( - Either.getOrThrow(parseToJourneyConfig({ oidc: minimalOidc })).realmPath, + Result.getOrThrow(parseToJourneyConfig({ oidc: minimalOidc })).realmPath, ).toBeUndefined(); }); it('parseToJourneyConfig_TimeoutPassedToServerConfig', () => { - const data = Either.getOrThrow(parseToJourneyConfig({ timeout: 10000, oidc: minimalOidc })); + const data = Result.getOrThrow(parseToJourneyConfig({ timeout: 10000, oidc: minimalOidc })); expect(data.serverConfig.timeout).toBe(10000); }); it('parseToJourneyConfig_OidcFieldsNotLeakedToResult', () => { - const data = Either.getOrThrow(parseToJourneyConfig(fullConfig)) as unknown as Record< + const data = Result.getOrThrow(parseToJourneyConfig(fullConfig)) as unknown as Record< string, unknown >; @@ -279,27 +279,27 @@ describe('parseToJourneyConfig', () => { }); it('parseToJourneyConfig_OidcMissingDiscoveryEndpoint_ReturnsFailure', () => { - const errors = Either.getOrThrow( - Either.flip(parseToJourneyConfig({ oidc: { realm: 'alpha' } })), + const errors = Result.getOrThrow( + Result.flip(parseToJourneyConfig({ oidc: { realm: 'alpha' } })), ); expect(errors.some((e) => e.field === 'oidc.discoveryEndpoint')).toBe(true); }); it('parseToJourneyConfig_NullInput_ReturnsFailure', () => { - expect(Either.isLeft(parseToJourneyConfig(null))).toBe(true); + expect(Result.isFailure(parseToJourneyConfig(null))).toBe(true); }); }); describe('parseToDavinciConfig', () => { it('parseToDavinciConfig_NoOidcBlock_ReturnsFailure', () => { - const errors = Either.getOrThrow( - Either.flip(parseToDavinciConfig({ journey: { serverUrl: 'https://example.com/am' } })), + const errors = Result.getOrThrow( + Result.flip(parseToDavinciConfig({ journey: { serverUrl: 'https://example.com/am' } })), ); expect(errors.some((e) => e.field === 'oidc')).toBe(true); }); it('parseToDavinciConfig_MinimalConfig_MapsRequiredFields', () => { - const data = Either.getOrThrow(parseToDavinciConfig({ oidc: minimalOidc })); + const data = Result.getOrThrow(parseToDavinciConfig({ oidc: minimalOidc })); expect(data.clientId).toBe('my-client'); expect(data.redirectUri).toBe('https://app.example.com/callback'); expect(data.scope).toBe('openid profile'); @@ -309,21 +309,21 @@ describe('parseToDavinciConfig', () => { }); it('parseToDavinciConfig_ScopesJoinedWithSpace', () => { - const data = Either.getOrThrow( + const data = Result.getOrThrow( parseToDavinciConfig({ oidc: { ...minimalOidc, scopes: ['openid', 'email'] } }), ); expect(data.scope).toBe('openid email'); }); it('parseToDavinciConfig_RefreshThresholdConvertedToMs', () => { - const data = Either.getOrThrow( + const data = Result.getOrThrow( parseToDavinciConfig({ oidc: { ...minimalOidc, refreshThreshold: 30 } }), ); expect(data.oauthThreshold).toBe(30000); }); it('parseToDavinciConfig_RealmMappedToRealmPath', () => { - const data = Either.getOrThrow( + const data = Result.getOrThrow( parseToDavinciConfig({ journey: { serverUrl: 'https://example.com/am', realm: 'alpha' }, oidc: minimalOidc, @@ -333,20 +333,20 @@ describe('parseToDavinciConfig', () => { }); it('parseToDavinciConfig_EmptyScopes_ReturnsFailure', () => { - const errors = Either.getOrThrow( - Either.flip(parseToDavinciConfig({ oidc: { ...minimalOidc, scopes: [] } })), + const errors = Result.getOrThrow( + Result.flip(parseToDavinciConfig({ oidc: { ...minimalOidc, scopes: [] } })), ); expect(errors.some((e) => e.field === 'oidc.scopes')).toBe(true); }); it('parseToDavinciConfig_TimeoutPassedToServerConfig', () => { - const data = Either.getOrThrow(parseToDavinciConfig({ timeout: 7000, oidc: minimalOidc })); + const data = Result.getOrThrow(parseToDavinciConfig({ timeout: 7000, oidc: minimalOidc })); expect(data.serverConfig.timeout).toBe(7000); }); it('parseToDavinciConfig_OidcMissingDiscoveryEndpoint_ReturnsFailure', () => { - const errors = Either.getOrThrow( - Either.flip( + const errors = Result.getOrThrow( + Result.flip( parseToDavinciConfig({ oidc: { clientId: 'x', redirectUri: 'x', scopes: ['openid'] } }), ), ); @@ -354,23 +354,23 @@ describe('parseToDavinciConfig', () => { }); it('parseToDavinciConfig_NullInput_ReturnsFailure', () => { - expect(Either.isLeft(parseToDavinciConfig(null))).toBe(true); + expect(Result.isFailure(parseToDavinciConfig(null))).toBe(true); }); }); describe('parseToOidcConfig log mapping', () => { it('parseToOidcConfig_LogFieldMapped_ToLogLevel', () => { - expect(Either.getOrThrow(parseToOidcConfig({ log: 'DEBUG', oidc: minimalOidc })).log).toBe( + expect(Result.getOrThrow(parseToOidcConfig({ log: 'DEBUG', oidc: minimalOidc })).log).toBe( 'debug', ); }); it('parseToOidcConfig_NoLogField_LogLevelAbsent', () => { - expect(Either.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })).log).toBeUndefined(); + expect(Result.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })).log).toBeUndefined(); }); it('parseToOidcConfig_CookieName_NotMappedToResult', () => { - const data = Either.getOrThrow( + const data = Result.getOrThrow( parseToOidcConfig({ journey: { serverUrl: 'https://example.com/am', cookieName: 'iPlanetDirectoryPro' }, oidc: minimalOidc, @@ -382,17 +382,17 @@ describe('parseToOidcConfig log mapping', () => { describe('parseToJourneyConfig log mapping', () => { it('parseToJourneyConfig_LogFieldMapped_ToLogLevel', () => { - expect(Either.getOrThrow(parseToJourneyConfig({ log: 'WARN', oidc: minimalOidc })).log).toBe( + expect(Result.getOrThrow(parseToJourneyConfig({ log: 'WARN', oidc: minimalOidc })).log).toBe( 'warn', ); }); it('parseToJourneyConfig_NoLogField_LogLevelAbsent', () => { - expect(Either.getOrThrow(parseToJourneyConfig({ oidc: minimalOidc })).log).toBeUndefined(); + expect(Result.getOrThrow(parseToJourneyConfig({ oidc: minimalOidc })).log).toBeUndefined(); }); it('parseToJourneyConfig_CookieName_NotMappedToResult', () => { - const data = Either.getOrThrow( + const data = Result.getOrThrow( parseToJourneyConfig({ journey: { serverUrl: 'https://example.com/am', cookieName: 'iPlanetDirectoryPro' }, oidc: minimalOidc, @@ -404,13 +404,13 @@ describe('parseToJourneyConfig log mapping', () => { describe('parseToDavinciConfig log mapping', () => { it('parseToDavinciConfig_LogFieldMapped_ToLogLevel', () => { - expect(Either.getOrThrow(parseToDavinciConfig({ log: 'ERROR', oidc: minimalOidc })).log).toBe( + expect(Result.getOrThrow(parseToDavinciConfig({ log: 'ERROR', oidc: minimalOidc })).log).toBe( 'error', ); }); it('parseToDavinciConfig_NoLogField_LogLevelAbsent', () => { - expect(Either.getOrThrow(parseToDavinciConfig({ oidc: minimalOidc })).log).toBeUndefined(); + expect(Result.getOrThrow(parseToDavinciConfig({ oidc: minimalOidc })).log).toBeUndefined(); }); }); @@ -520,22 +520,22 @@ describe('makeDavinciConfig', () => { describe('collectErrors', () => { it('collectErrors_AllRight_ReturnsEmpty', () => { - expect(collectErrors([Either.right(1), Either.right('a')])).toEqual([]); + expect(collectErrors([Result.succeed(1), Result.succeed('a')])).toEqual([]); }); it('collectErrors_MultipleLeft_AccumulatesAllErrors', () => { const errors = collectErrors([ - Either.right(1), - Either.left([{ field: 'a', message: 'bad a' }]), - Either.left([{ field: 'b', message: 'bad b' }]), + Result.succeed(1), + Result.fail([{ field: 'a', message: 'bad a' }]), + Result.fail([{ field: 'b', message: 'bad b' }]), ]); expect(errors.map((e) => e.field)).toEqual(['a', 'b']); }); it('collectErrors_DoesNotShortCircuit', () => { const errors = collectErrors([ - Either.left([{ field: 'first', message: 'x' }]), - Either.left([{ field: 'second', message: 'y' }]), + Result.fail([{ field: 'first', message: 'x' }]), + Result.fail([{ field: 'second', message: 'y' }]), ]); expect(errors).toHaveLength(2); }); @@ -548,7 +548,7 @@ describe('parseOidcSection', () => { clientId: 'my-client', scopes: ['openid'], }); - expect(Either.getOrThrow(result).clientId).toBe('my-client'); + expect(Result.getOrThrow(result).clientId).toBe('my-client'); }); it('parseOidcSection_UnknownField_Ignored', () => { @@ -556,11 +556,11 @@ describe('parseOidcSection', () => { discoveryEndpoint: 'https://example.com/.well-known', unknownField: 'kept', }); - expect(Either.isRight(result)).toBe(true); + expect(Result.isSuccess(result)).toBe(true); }); it('parseOidcSection_MissingDiscoveryEndpoint_ReturnsError', () => { - const errors = Either.getOrThrow(Either.flip(parseOidcSection({ clientId: 'my-client' }))); + const errors = Result.getOrThrow(Result.flip(parseOidcSection({ clientId: 'my-client' }))); expect(errors.some((e) => e.field === 'oidc.discoveryEndpoint')).toBe(true); }); }); diff --git a/packages/sdk-utilities/src/lib/config/config.types.ts b/packages/sdk-utilities/src/lib/config/config.types.ts index bbcf2c2be1..3706344f77 100644 --- a/packages/sdk-utilities/src/lib/config/config.types.ts +++ b/packages/sdk-utilities/src/lib/config/config.types.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ -import type * as Either from 'effect/Either'; +import type { Result } from 'effect'; import type { LogLevel, AuthDisplayValue, AuthPromptValue } from '@forgerock/sdk-types'; @@ -50,10 +50,10 @@ export type ConfigValidationError = { }; /** - * A parsed result over the accumulating-error channel. Effect's `Either` is - * `Either`, so the SECOND type parameter is the error channel. + * A parsed result over the accumulating-error channel. Effect's `Result` is + * `Result`, so the SECOND type parameter is the error channel. */ -export type ParseResult = Either.Either; +export type ParseResult = Result.Result; /** Parses a record of unknown values into `A`. Unknown fields are silently ignored. */ export type Parser = (input: Readonly>) => ParseResult; diff --git a/packages/sdk-utilities/src/lib/config/config.utils.ts b/packages/sdk-utilities/src/lib/config/config.utils.ts index 576d1b42f5..d6adad56de 100644 --- a/packages/sdk-utilities/src/lib/config/config.utils.ts +++ b/packages/sdk-utilities/src/lib/config/config.utils.ts @@ -5,8 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ -import { pipe } from 'effect'; -import * as Either from 'effect/Either'; +import { pipe, Result } from 'effect'; import { AUTH_DISPLAY_VALUES, @@ -69,22 +68,22 @@ function isAbsent(value: unknown): value is undefined | null { /** * Gather every error from a list of parsed results without short-circuiting (unlike - * `Either.all`, which stops at the first `Left`). Returns all accumulated errors so a + * `Result.all`, which stops at the first failure). Returns all accumulated errors so a * section parser can report every invalid field in one pass. */ export function collectErrors( results: ReadonlyArray>, ): ConfigValidationError[] { - return results.flatMap((result) => (Either.isLeft(result) ? result.left : [])); + return results.flatMap((result) => (Result.isFailure(result) ? result.failure : [])); } /** * Unwraps a `ParseResult`, then returns `{ [key]: value }` when the value is defined, `{}` otherwise. * Spread into an object literal to conditionally include a property without an inline ternary. - * Safe to call after `collectErrors` — every result is guaranteed `Right` past the error guard. + * Safe to call after `collectErrors` — every result is guaranteed `Success` past the error guard. */ function parsedProp(key: K, result: ParseResult): ParsedProp { - const value = Either.getOrThrow(result); + const value = Result.getOrThrow(result); return (value !== undefined ? { [key]: value } : {}) as ParsedProp; } @@ -94,21 +93,21 @@ function parsedProp(key: K, result: ParseResult): Parsed const requiredString: FieldParser = (value, fieldPath) => { if (isAbsent(value)) { - return Either.left([{ field: fieldPath, message: 'Required field is missing' }]); + return Result.fail([{ field: fieldPath, message: 'Required field is missing' }]); } return typeof value === 'string' - ? Either.right(value) - : Either.left([{ field: fieldPath, message: `Expected string, got ${typeName(value)}` }]); + ? Result.succeed(value) + : Result.fail([{ field: fieldPath, message: `Expected string, got ${typeName(value)}` }]); }; /** Required, non-empty string — treats `''` as missing (a blank value can't satisfy a requirement). */ const requiredNonEmptyString: FieldParser = (value, fieldPath) => { if (isAbsent(value) || value === '') { - return Either.left([{ field: fieldPath, message: 'Required field is missing' }]); + return Result.fail([{ field: fieldPath, message: 'Required field is missing' }]); } return typeof value === 'string' - ? Either.right(value) - : Either.left([{ field: fieldPath, message: `Expected string, got ${typeName(value)}` }]); + ? Result.succeed(value) + : Result.fail([{ field: fieldPath, message: `Expected string, got ${typeName(value)}` }]); }; /** Parse each element of an array as a string, accumulating one error per non-string element. */ @@ -125,46 +124,46 @@ function parseStringElements(value: readonly unknown[], fieldPath: string): Pars }); } }); - return errors.length > 0 ? Either.left(errors) : Either.right(parsed); + return errors.length > 0 ? Result.fail(errors) : Result.succeed(parsed); } /** Required, non-empty array of strings — treats absent or `[]` as missing. */ const requiredNonEmptyStringArray: FieldParser = (value, fieldPath) => { if (isAbsent(value) || (Array.isArray(value) && value.length === 0)) { - return Either.left([{ field: fieldPath, message: 'Required field is missing' }]); + return Result.fail([{ field: fieldPath, message: 'Required field is missing' }]); } if (!Array.isArray(value)) { - return Either.left([{ field: fieldPath, message: `Expected array, got ${typeName(value)}` }]); + return Result.fail([{ field: fieldPath, message: `Expected array, got ${typeName(value)}` }]); } return parseStringElements(value, fieldPath); }; const optionalString: FieldParser = (value, fieldPath) => { - if (isAbsent(value)) return Either.right(undefined); + if (isAbsent(value)) return Result.succeed(undefined); return typeof value === 'string' - ? Either.right(value) - : Either.left([{ field: fieldPath, message: `Expected string, got ${typeName(value)}` }]); + ? Result.succeed(value) + : Result.fail([{ field: fieldPath, message: `Expected string, got ${typeName(value)}` }]); }; /** Finite, non-negative number (rejects NaN, Infinity, negatives). Optional-aware. */ const finiteNonNegativeNumber: FieldParser = (value, fieldPath) => { - if (isAbsent(value)) return Either.right(undefined); + if (isAbsent(value)) return Result.succeed(undefined); if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) { - return Either.left([ + return Result.fail([ { field: fieldPath, message: `Expected a finite, non-negative number, got ${typeName(value)}`, }, ]); } - return Either.right(value); + return Result.succeed(value); }; /** Optional array of strings, accumulating one error per non-string element. */ const optionalStringArray: FieldParser = (value, fieldPath) => { - if (isAbsent(value)) return Either.right(undefined); + if (isAbsent(value)) return Result.succeed(undefined); if (!Array.isArray(value)) { - return Either.left([{ field: fieldPath, message: `Expected array, got ${typeName(value)}` }]); + return Result.fail([{ field: fieldPath, message: `Expected array, got ${typeName(value)}` }]); } return parseStringElements(value, fieldPath); }; @@ -174,9 +173,9 @@ const optionalStringRecord: FieldParser | undefined> = ( value, fieldPath, ) => { - if (isAbsent(value)) return Either.right(undefined); + if (isAbsent(value)) return Result.succeed(undefined); if (typeof value !== 'object' || Array.isArray(value)) { - return Either.left([{ field: fieldPath, message: `Expected object, got ${typeName(value)}` }]); + return Result.fail([{ field: fieldPath, message: `Expected object, got ${typeName(value)}` }]); } const parsed: Record = {}; const errors: ConfigValidationError[] = []; @@ -190,7 +189,7 @@ const optionalStringRecord: FieldParser | undefined> = ( }); } } - return errors.length > 0 ? Either.left(errors) : Either.right(parsed); + return errors.length > 0 ? Result.fail(errors) : Result.succeed(parsed); }; /** @@ -201,16 +200,16 @@ function optionalLiteralUnion( members: Members, ): FieldParser { return (value, fieldPath) => { - if (isAbsent(value)) return Either.right(undefined); + if (isAbsent(value)) return Result.succeed(undefined); if (typeof value !== 'string') { - return Either.left([ + return Result.fail([ { field: fieldPath, message: `Expected string, got ${typeName(value)}` }, ]); } const matched = members.find((member): member is Members[number] => member === value); return matched !== undefined - ? Either.right(matched) - : Either.left([ + ? Result.succeed(matched) + : Result.fail([ { field: fieldPath, message: `Expected one of ${members.join(', ')}, got ${value}` }, ]); }; @@ -260,10 +259,10 @@ export const parseOidcSection: Parser = (input) => { acrValues, additionalParameters, ]); - if (errors.length > 0) return Either.left(errors); + if (errors.length > 0) return Result.fail(errors); const oidc: UnifiedOidcConfig = { - discoveryEndpoint: Either.getOrThrow(discoveryEndpoint), + discoveryEndpoint: Result.getOrThrow(discoveryEndpoint), ...parsedProp('clientId', clientId), ...parsedProp('redirectUri', redirectUri), ...parsedProp('scopes', scopes), @@ -277,7 +276,7 @@ export const parseOidcSection: Parser = (input) => { ...parsedProp('acrValues', acrValues), ...parsedProp('additionalParameters', additionalParameters), }; - return Either.right(oidc); + return Result.succeed(oidc); }; /** Parse the `journey` block. `serverUrl` required; `realm`/`cookieName` optional. */ @@ -287,28 +286,28 @@ export const parseJourneySection: Parser = (input) => { const cookieName = optionalString(input['cookieName'], 'journey.cookieName'); const errors = collectErrors([serverUrl, realm, cookieName]); - if (errors.length > 0) return Either.left(errors); + if (errors.length > 0) return Result.fail(errors); const journey: UnifiedJourneyConfig = { - serverUrl: Either.getOrThrow(serverUrl), + serverUrl: Result.getOrThrow(serverUrl), ...parsedProp('realm', realm), ...parsedProp('cookieName', cookieName), }; - return Either.right(journey); + return Result.succeed(journey); }; /** - * Run a section parser against an optional nested object: absent → `Right(undefined)`; - * present-but-not-an-object → `Left`; present object → delegate to `parser`. + * Run a section parser against an optional nested object: absent → `Result.succeed(undefined)`; + * present-but-not-an-object → failure; present object → delegate to `parser`. */ function parseOptionalSection( value: unknown, prefix: string, parser: Parser, ): ParseResult { - if (isAbsent(value)) return Either.right(undefined); + if (isAbsent(value)) return Result.succeed(undefined); if (typeof value !== 'object' || Array.isArray(value)) { - return Either.left([{ field: prefix, message: `Expected object, got ${typeName(value)}` }]); + return Result.fail([{ field: prefix, message: `Expected object, got ${typeName(value)}` }]); } return parser({ ...value }); } @@ -321,7 +320,7 @@ export const parseUnifiedSdkConfig: Parser = (input) => { const oidc = parseOptionalSection(input['oidc'], 'oidc', parseOidcSection); const errors = collectErrors([timeout, log, journey, oidc]); - if (errors.length > 0) return Either.left(errors); + if (errors.length > 0) return Result.fail(errors); const config: UnifiedSdkConfig = { ...parsedProp('timeout', timeout), @@ -329,7 +328,7 @@ export const parseUnifiedSdkConfig: Parser = (input) => { ...parsedProp('journey', journey), ...parsedProp('oidc', oidc), }; - return Either.right(config); + return Result.succeed(config); }; /* ------------------------------------------------------------------ * @@ -344,14 +343,14 @@ export const parseUnifiedSdkConfig: Parser = (input) => { */ function parseClientSdkConfig(config: UnifiedSdkConfig): ParseResult { if (!config.oidc) { - return Either.left([{ field: 'oidc', message: 'Required block is missing' }]); + return Result.fail([{ field: 'oidc', message: 'Required block is missing' }]); } const oidc = config.oidc; - // All three are required and non-optional, so `Either.all` (first-error) is enough — the + // All three are required and non-optional, so `Result.all` (first-error) is enough — the // struct form keeps field/value paired by key. Section parsers collect errors across many // optional fields instead, so they accumulate via `collectErrors`. - return Either.map( - Either.all({ + return Result.map( + Result.all({ clientId: requiredNonEmptyString(oidc.clientId, 'oidc.clientId'), redirectUri: requiredNonEmptyString(oidc.redirectUri, 'oidc.redirectUri'), scopes: requiredNonEmptyStringArray(oidc.scopes, 'oidc.scopes'), @@ -367,8 +366,8 @@ function parseClientSdkConfig(config: UnifiedSdkConfig): ParseResult { return config.oidc - ? Either.right({ ...config, oidc: config.oidc }) - : Either.left([{ field: 'oidc', message: 'Required block is missing' }]); + ? Result.succeed({ ...config, oidc: config.oidc }) + : Result.fail([{ field: 'oidc', message: 'Required block is missing' }]); } /* ------------------------------------------------------------------ * @@ -440,33 +439,33 @@ function buildDavinciConfig(config: ClientSdkConfig): DaVinciConfig { function assertObject( input: unknown, -): Either.Either>, ConfigValidationError[]> { +): Result.Result>, ConfigValidationError[]> { if (typeof input !== 'object' || input === null || Array.isArray(input)) { - return Either.left([{ field: 'config', message: `Expected object, got ${typeName(input)}` }]); + return Result.fail([{ field: 'config', message: `Expected object, got ${typeName(input)}` }]); } - return Either.right(input as Readonly>); + return Result.succeed(input as Readonly>); } export const parseToOidcConfig = (input: unknown): ParseResult => pipe( assertObject(input), - Either.flatMap(parseUnifiedSdkConfig), - Either.flatMap(parseClientSdkConfig), - Either.map(buildOidcConfig), + Result.flatMap(parseUnifiedSdkConfig), + Result.flatMap(parseClientSdkConfig), + Result.map(buildOidcConfig), ); export const parseToJourneyConfig = (input: unknown): ParseResult => pipe( assertObject(input), - Either.flatMap(parseUnifiedSdkConfig), - Either.flatMap(parseJourneySdkConfig), - Either.map(buildJourneyConfig), + Result.flatMap(parseUnifiedSdkConfig), + Result.flatMap(parseJourneySdkConfig), + Result.map(buildJourneyConfig), ); export const parseToDavinciConfig = (input: unknown): ParseResult => pipe( assertObject(input), - Either.flatMap(parseUnifiedSdkConfig), - Either.flatMap(parseClientSdkConfig), - Either.map(buildDavinciConfig), + Result.flatMap(parseUnifiedSdkConfig), + Result.flatMap(parseClientSdkConfig), + Result.map(buildDavinciConfig), ); diff --git a/packages/sdk-utilities/src/lib/micro.utils.ts b/packages/sdk-utilities/src/lib/micro.utils.ts index f5febe126d..3fc9f73be0 100644 --- a/packages/sdk-utilities/src/lib/micro.utils.ts +++ b/packages/sdk-utilities/src/lib/micro.utils.ts @@ -4,22 +4,32 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { causeIsDie, exitIsFail, exitIsSuccess } from 'effect/Micro'; -import type { MicroExit } from 'effect/Micro'; +import { Cause, Exit } from 'effect'; import type { GenericError } from '@forgerock/sdk-types'; -export function handleMicroExit( - result: MicroExit, +/** + * Unwrap an {@link Exit.Exit} into a plain value. + * + * - **Success** → returns the wrapped value. + * - **Failure with a typed error** → returns the error value from the first `Fail` reason. + * - **Defect / Die** → returns a {@link GenericError} built from the defect message. + * - **Other failure** → returns a {@link GenericError} with an unknown defect message. + */ +export function handleExit( + result: Exit.Exit, defectError: string, defectType: GenericError['type'], ): T | E | GenericError { - if (exitIsSuccess(result)) { + if (Exit.isSuccess(result)) { return result.value; } - if (exitIsFail(result)) { - return result.cause.error; + const reasons = result.cause.reasons; + const failReason = reasons.find(Cause.isFailReason); + if (failReason !== undefined) { + return failReason.error; } - const defect = causeIsDie(result.cause) ? result.cause.defect : undefined; + const dieReason = reasons.find(Cause.isDieReason); + const defect = dieReason?.defect; return { error: defectError, message: defect instanceof Error ? defect.message : String(defect ?? 'Unknown defect'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d63d00b9b8..6409c0db08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,41 +31,38 @@ catalogs: specifier: 4.21.0 version: 4.21.0 effect: - '@effect/cli': - specifier: ^0.69.0 - version: 0.69.2 '@effect/language-service': - specifier: ^0.35.2 - version: 0.35.2 + specifier: 0.87.1 + version: 0.87.1 '@effect/opentelemetry': - specifier: ^0.56.1 - version: 0.56.6 - '@effect/platform': - specifier: ^0.90.0 - version: 0.90.10 + specifier: 4.0.0-beta.103 + version: 4.0.0-beta.103 '@effect/platform-node': - specifier: 0.94.2 - version: 0.94.2 + specifier: 4.0.0-beta.103 + version: 4.0.0-beta.103 '@effect/vitest': - specifier: ^0.27.0 - version: 0.27.0 + specifier: 4.0.0-beta.103 + version: 4.0.0-beta.103 effect: - specifier: ^3.20.0 - version: 3.21.0 + specifier: 4.0.0-beta.103 + version: 4.0.0-beta.103 vite: vite: specifier: ^7.3.6 version: 7.3.6 vitest: '@vitest/coverage-v8': - specifier: ^3.2.6 - version: 3.2.6 + specifier: ^4.1.0 + version: 4.1.10 + '@vitest/ui': + specifier: ^4.1.0 + version: 4.1.10 vitest: - specifier: ^3.2.6 - version: 3.2.6 + specifier: ^4.1.0 + version: 4.1.10 vitest-canvas-mock: - specifier: ^1.1.3 - version: 1.1.3 + specifier: ^1.1.4 + version: 1.1.4 overrides: rollup: ^4.59.0 @@ -99,9 +96,6 @@ importers: '@commitlint/prompt': specifier: ^20.0.0 version: 20.1.0(@types/node@24.9.2)(typescript@5.8.3) - '@effect/cli': - specifier: catalog:effect - version: 0.69.2(@effect/platform@0.90.10(effect@3.21.0))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) '@eslint/eslintrc': specifier: ^3.0.0 version: 3.3.5 @@ -137,13 +131,13 @@ importers: version: 22.7.6(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(@types/node@24.9.2)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.39.4(jiti@2.6.1))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.21))(@types/node@24.9.2)(typescript@5.8.3))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0)) '@nx/vite': specifier: 22.7.6 - version: 22.7.6(@babel/traverse@7.28.5)(@nx/eslint@22.7.6(03cdb71bd33024bc72407dcf2fd22241))(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@3.2.6) + version: 22.7.6(@babel/traverse@7.28.5)(@nx/eslint@22.7.6(03cdb71bd33024bc72407dcf2fd22241))(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10) '@nx/vitest': specifier: 22.7.6 - version: 22.7.6(@babel/traverse@7.28.5)(@nx/eslint@22.7.6(03cdb71bd33024bc72407dcf2fd22241))(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@3.2.6) + version: 22.7.6(@babel/traverse@7.28.5)(@nx/eslint@22.7.6(03cdb71bd33024bc72407dcf2fd22241))(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10) '@nx/web': specifier: 22.7.6 - version: 22.7.6(04f16120cd0b4809abbf447c38c7f725) + version: 22.7.6(294da7a21825b430d5914a528d2ef8ee) '@nx/workspace': specifier: 22.7.6 version: 22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)) @@ -188,10 +182,10 @@ importers: version: 8.46.3(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3) '@vitest/coverage-v8': specifier: catalog:vitest - version: 3.2.6(vitest@3.2.6) + version: 4.1.10(vitest@4.1.10) '@vitest/ui': - specifier: 3.2.6 - version: 3.2.6(vitest@3.2.6) + specifier: catalog:vitest + version: 4.1.10(vitest@4.1.10) conventional-changelog-conventionalcommits: specifier: ^8.0.0 version: 8.0.0 @@ -290,10 +284,10 @@ importers: version: 7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) vitest: specifier: catalog:vitest - version: 3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) vitest-canvas-mock: specifier: catalog:vitest - version: 1.1.3(vitest@3.2.6) + version: 1.1.4(vitest@4.1.10) e2e/am-mock-api: dependencies: @@ -347,11 +341,11 @@ importers: version: 4.9.0 effect: specifier: catalog:effect - version: 3.21.0 + version: 4.0.0-beta.103 devDependencies: '@effect/language-service': specifier: catalog:effect - version: 0.35.2 + version: 0.87.1 e2e/journey-app: dependencies: @@ -377,16 +371,13 @@ importers: dependencies: '@effect/language-service': specifier: catalog:effect - version: 0.35.2 + version: 0.87.1 '@effect/opentelemetry': specifier: catalog:effect - version: 0.56.6(@effect/platform@0.90.10(effect@3.21.0))(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(effect@3.21.0) - '@effect/platform': - specifier: catalog:effect - version: 0.90.10(effect@3.21.0) + version: 4.0.0-beta.103(@opentelemetry/api-logs@0.207.0)(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(effect@4.0.0-beta.103) '@effect/platform-node': specifier: catalog:effect - version: 0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) + version: 4.0.0-beta.103(effect@4.0.0-beta.103)(ioredis@5.11.1) '@opentelemetry/sdk-logs': specifier: 0.207.0 version: 0.207.0(@opentelemetry/api@1.9.0) @@ -404,17 +395,17 @@ importers: version: 2.2.0(@opentelemetry/api@1.9.0) effect: specifier: catalog:effect - version: 3.21.0 + version: 4.0.0-beta.103 nanoid: specifier: 5.1.9 version: 5.1.9 devDependencies: '@effect/vitest': specifier: catalog:effect - version: 0.27.0(effect@3.21.0)(vitest@3.2.6) + version: 4.0.0-beta.103(effect@4.0.0-beta.103)(vitest@4.1.10) vitest: specifier: catalog:vitest - version: 3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) e2e/oidc-app: dependencies: @@ -463,17 +454,17 @@ importers: version: 2.10.1 effect: specifier: catalog:effect - version: 3.21.0 + version: 4.0.0-beta.103 immer: specifier: 'catalog:' version: 10.2.0 devDependencies: '@effect/vitest': specifier: catalog:effect - version: 0.27.0(effect@3.21.0)(vitest@3.2.6) + version: 4.0.0-beta.103(effect@4.0.0-beta.103)(vitest@4.1.10) vitest: specifier: catalog:vitest - version: 3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) packages/device-client: dependencies: @@ -516,23 +507,23 @@ importers: version: 2.10.1 effect: specifier: catalog:effect - version: 3.21.0 + version: 4.0.0-beta.103 tslib: specifier: 'catalog:' version: 2.8.1 devDependencies: '@vitest/coverage-v8': specifier: catalog:vitest - version: 3.2.6(vitest@3.2.6) + version: 4.1.10(vitest@4.1.10) vite: specifier: catalog:vite version: 7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) vitest: specifier: catalog:vitest - version: 3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) vitest-canvas-mock: specifier: catalog:vitest - version: 1.1.3(vitest@3.2.6) + version: 1.1.4(vitest@4.1.10) packages/oidc-client: dependencies: @@ -565,14 +556,14 @@ importers: version: 2.10.1 effect: specifier: catalog:effect - version: 3.21.0 + version: 4.0.0-beta.103 jose: specifier: 'catalog:' version: 6.2.3 devDependencies: '@effect/vitest': specifier: catalog:effect - version: 0.27.0(effect@3.21.0)(vitest@3.2.6) + version: 4.0.0-beta.103(effect@4.0.0-beta.103)(vitest@4.1.10) msw: specifier: 'catalog:' version: 2.12.1(@types/node@24.9.2)(typescript@5.9.3) @@ -629,7 +620,7 @@ importers: version: link:../sdk-types effect: specifier: catalog:effect - version: 3.21.0 + version: 4.0.0-beta.103 scratchpad: dependencies: @@ -660,7 +651,7 @@ importers: version: 28.0.0 vitest: specifier: catalog:vitest - version: 3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) devDependencies: '@forgerock/javascript-sdk': specifier: 'catalog:' @@ -668,37 +659,31 @@ importers: tools/release: dependencies: - '@effect/platform': - specifier: catalog:effect - version: 0.90.10(effect@3.21.0) '@effect/platform-node': specifier: catalog:effect - version: 0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) + version: 4.0.0-beta.103(effect@4.0.0-beta.103)(ioredis@5.11.1) effect: specifier: catalog:effect - version: 3.21.0 + version: 4.0.0-beta.103 tools/user-scripts: dependencies: - '@effect/platform': - specifier: catalog:effect - version: 0.90.10(effect@3.21.0) '@effect/platform-node': specifier: catalog:effect - version: 0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) + version: 4.0.0-beta.103(effect@4.0.0-beta.103)(ioredis@5.11.1) effect: specifier: catalog:effect - version: 3.21.0 + version: 4.0.0-beta.103 vitest: specifier: catalog:vitest - version: 3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) devDependencies: '@effect/language-service': specifier: catalog:effect - version: 0.35.2 + version: 0.87.1 '@effect/vitest': specifier: catalog:effect - version: 0.27.0(effect@3.21.0)(vitest@3.2.6) + version: 4.0.0-beta.103(effect@4.0.0-beta.103)(vitest@4.1.10) packages: @@ -720,10 +705,6 @@ packages: '@altano/repository-tools@2.0.3': resolution: {integrity: sha512-cSR/ZYDF6Wp9OeAJMyLYYN1GenAAhV17W+w38ELP+3c5Ltsy9jkkCymi33nz/qnXyef3n6Fbr1h2yt3dvUN5sQ==} - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@asamuzakjp/css-color@4.1.2': resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} @@ -820,10 +801,18 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -841,6 +830,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} engines: {node: '>=6.9.0'} @@ -1343,6 +1337,10 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -1535,56 +1533,29 @@ packages: resolution: {integrity: sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==} engines: {node: '>=18'} - '@effect/cli@0.69.2': - resolution: {integrity: sha512-1xYfEzW5f6jGWDb6V6DWTsWbtdp8DgQcchbewnCwKOXnJzF5VsTcfo4lYM4TXk/RAHA1owyq7OaEmeZ+qIt1/w==} - peerDependencies: - '@effect/platform': ^0.90.5 - '@effect/printer': ^0.45.0 - '@effect/printer-ansi': ^0.45.0 - effect: ^3.17.8 - - '@effect/cluster@0.46.4': - resolution: {integrity: sha512-81nWw5ABtRZZFmQTrWvfsUnqTg9LybIFYvmsiIL7xQ2t+g6746IZe9Tcv9bSmMdioJLgeuO4CiRg0FfDP2qCWA==} - peerDependencies: - '@effect/platform': ^0.90.0 - '@effect/rpc': ^0.68.3 - '@effect/sql': ^0.44.1 - '@effect/workflow': ^0.8.3 - effect: ^3.17.6 - - '@effect/experimental@0.54.6': - resolution: {integrity: sha512-UqHMvCQmrZT6kUVoUC0lqyno4Yad+j9hBGCdUjW84zkLwAq08tPqySiZUKRwY+Ae5B2Ab8rISYJH7nQvct9DMQ==} - peerDependencies: - '@effect/platform': ^0.90.2 - effect: ^3.17.7 - ioredis: ^5 - lmdb: ^3 - peerDependenciesMeta: - ioredis: - optional: true - lmdb: - optional: true - - '@effect/language-service@0.35.2': - resolution: {integrity: sha512-J7GbtthuYeruD4kYUHn3QEZtbl9v7OX9+ElD20mDBGBMA+Q6W4KnVMxZc+yDvKQBBYvfXImVUSzBbXzbrZJpyg==} + '@effect/language-service@0.87.1': + resolution: {integrity: sha512-kcljlJmEgqg5mFAM6UShJYJjMqJb3TbHHxrK8Qoubvwugc0aVWpRkbdgQvK5b17puOt3BKXXKOmcV+oQt0oQqQ==} hasBin: true - '@effect/opentelemetry@0.56.6': - resolution: {integrity: sha512-cBi9frXujTIEGXChkl4VdQfvDe7QvzC18SM8wK0CKYSgH9ZL7v/F5f5/3fTSTfEdO9ZyBk73s5Jbbogab0Q01g==} + '@effect/opentelemetry@4.0.0-beta.103': + resolution: {integrity: sha512-nGen5/SeuCkSII+X71YjCUsT/r1AqfS6eWyzCTzXv0NqyLxtnpWJxSYBGy5LtwZFMQsTmQKC7R426IzXbsxOXg==} + engines: {node: '>=18.0.0'} peerDependencies: - '@effect/platform': ^0.90.8 '@opentelemetry/api': ^1.9 + '@opentelemetry/api-logs': '>=0.203.0 <0.300.0' '@opentelemetry/resources': ^2.0.0 - '@opentelemetry/sdk-logs': ^0.203.0 + '@opentelemetry/sdk-logs': '>=0.203.0 <0.300.0' '@opentelemetry/sdk-metrics': ^2.0.0 '@opentelemetry/sdk-trace-base': ^2.0.0 '@opentelemetry/sdk-trace-node': ^2.0.0 '@opentelemetry/sdk-trace-web': ^2.0.0 '@opentelemetry/semantic-conventions': ^1.33.0 - effect: ^3.17.13 + effect: ^4.0.0-beta.103 peerDependenciesMeta: '@opentelemetry/api': optional: true + '@opentelemetry/api-logs': + optional: true '@opentelemetry/resources': optional: true '@opentelemetry/sdk-logs': @@ -1598,71 +1569,24 @@ packages: '@opentelemetry/sdk-trace-web': optional: true - '@effect/platform-node-shared@0.47.2': - resolution: {integrity: sha512-mtXNAx7Rzbfmp8hsMnyYebIkdCoKKOMa61uRLfOgbOV0p/ksO99YHYZSE/UTE2fFeoF9f2oi0mwOj/G7EMqzng==} - peerDependencies: - '@effect/cluster': ^0.46.4 - '@effect/platform': ^0.90.0 - '@effect/rpc': ^0.68.3 - '@effect/sql': ^0.44.1 - effect: ^3.17.6 - - '@effect/platform-node@0.94.2': - resolution: {integrity: sha512-iI7vUjNqd1DOFCa/9Tyf6Cu00Y4oLKMrpa2lx8+bUIHxtYbk696Yd9VFIDLMXVWrKFUru4Fw7WgWaA/YDor/sw==} - peerDependencies: - '@effect/cluster': ^0.46.4 - '@effect/platform': ^0.90.0 - '@effect/rpc': ^0.68.3 - '@effect/sql': ^0.44.1 - effect: ^3.17.6 - - '@effect/platform@0.90.10': - resolution: {integrity: sha512-QhDPgCaLfIMQKOCoCPQvRUS+Y34iYJ07jdZ/CBAvYFvg/iUBebsmFuHL63RCD/YZH9BuK/kqqLYAA3M0fmUEgg==} - peerDependencies: - effect: ^3.17.13 - - '@effect/printer-ansi@0.45.0': - resolution: {integrity: sha512-3MS02RP83eZaBJX98PRI4f5kyoEVyNfg2Qu/XUWQMFRp4wvmgNwEy18RjO9G6s7uB8NaYXTpQVDmtUoKARx7fA==} - peerDependencies: - '@effect/typeclass': ^0.36.0 - effect: ^3.17.0 - - '@effect/printer@0.45.0': - resolution: {integrity: sha512-UpFBH2JKAgakSWpue6yKkIAXMq+3md/CPb9s/NGl28vDu1P33cvDeeDL/1EOzFk8WqhIs3oKwPMDnd3jUhjzdg==} - peerDependencies: - '@effect/typeclass': ^0.36.0 - effect: ^3.17.0 - - '@effect/rpc@0.68.4': - resolution: {integrity: sha512-iFGqBtZjjatNWwgDCCajYZvQSHc55XyZEdmHGqrKS6UfdRsV7qUSPgnplMf++tIIyGme7IbAjD/3VJdbHI5Gzg==} - peerDependencies: - '@effect/platform': ^0.90.2 - effect: ^3.17.7 - - '@effect/sql@0.44.2': - resolution: {integrity: sha512-DEcvriHvj88zu7keruH9NcHQzam7yQzLNLJO6ucDXMCAwWzYZSJOsmkxBznRFv8ylFtccSclKH2fuj+wRKPjCQ==} - peerDependencies: - '@effect/experimental': ^0.54.6 - '@effect/platform': ^0.90.4 - effect: ^3.17.7 - - '@effect/typeclass@0.36.0': - resolution: {integrity: sha512-+8xYvX4tjD7gKwGYzOyFh90I+ptdXzoNHLQTSa8kGh/xOVZMIGYb0VgLoNHE02UsuVrB+JJJuBmKLdd5TeDTPg==} + '@effect/platform-node-shared@4.0.0-beta.103': + resolution: {integrity: sha512-0aCZMBid5ifqmY55TkfCDLaGTIM8qu3bNFUW7qL9vh/7jFOkaIAMX2MA8muG4deqW17XWxawddWu4v0fK+UW3g==} + engines: {node: '>=18.0.0'} peerDependencies: - effect: ^3.17.0 + effect: ^4.0.0-beta.103 - '@effect/vitest@0.27.0': - resolution: {integrity: sha512-8bM7n9xlMUYw9GqPIVgXFwFm2jf27m/R7psI64PGpwU5+26iwyxp9eAXEsfT5S6lqztYfpQQ1Ubp5o6HfNYzJQ==} + '@effect/platform-node@4.0.0-beta.103': + resolution: {integrity: sha512-VD8fbpendwFokMwzC7/MxazjhEVDihPC5NZtomYEyZwGaA1LXMvwP1y8pfXwfshUkG56TN4EMQqzu72c0B9FhA==} + engines: {node: '>=18.0.0'} peerDependencies: - effect: ^3.19.0 - vitest: ^3.2.0 + effect: ^4.0.0-beta.103 + ioredis: ^5.7.0 - '@effect/workflow@0.8.3': - resolution: {integrity: sha512-8X5IOemCb6I66GMd84w6NSmaQ+Ya3oXwItCUMelQAEuRtGzwqsw8PNNunQgK/poSRkmpszlsKOb6kEVNjSdFiQ==} + '@effect/vitest@4.0.0-beta.103': + resolution: {integrity: sha512-Kz3gemVuJNAZ3e4V6A7BwAP87x2Av8LyHOlCjy9jbZzlncwJXO7Olk1Sje3hdKaet3wEl51A/0/89xJ2IYSgNg==} peerDependencies: - '@effect/platform': ^0.90.0 - '@effect/rpc': ^0.68.3 - effect: ^3.17.6 + effect: ^4.0.0-beta.103 + vitest: ^4.1.0 '@emnapi/core@1.4.5': resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} @@ -1961,6 +1885,9 @@ packages: '@types/node': optional: true + '@ioredis/commands@1.10.0': + resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2094,33 +2021,33 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} cpu: [arm64] os: [darwin] - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} cpu: [x64] os: [darwin] - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} cpu: [arm64] os: [linux] - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} cpu: [arm] os: [linux] - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} cpu: [x64] os: [linux] - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} cpu: [x64] os: [win32] @@ -2648,88 +2575,6 @@ packages: '@paralleldrive/cuid2@2.2.2': resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} - '@parcel/watcher-android-arm64@2.5.1': - resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.1': - resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.1': - resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.1': - resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.1': - resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm-musl@2.5.1': - resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm64-glibc@2.5.1': - resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-arm64-musl@2.5.1': - resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-x64-glibc@2.5.1': - resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-linux-x64-musl@2.5.1': - resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-win32-arm64@2.5.1': - resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.1': - resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.1': - resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.1': - resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} - engines: {node: '>= 10.0.0'} - '@phenomnomnominal/tsquery@6.2.0': resolution: {integrity: sha512-Vo9nkhfZxDB/sBiqIY3pjDC4mOSyure+AFlEW5hcy/tRE82MqCXjRN4InnVNMldinRt0dLYqg4HAU2XPq5e1LA==} peerDependencies: @@ -3235,6 +3080,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -3496,48 +3344,48 @@ packages: resolution: {integrity: sha512-wfwn3z5M+w2KOV+xJFVv8tM8aOB4Ok5emfBDrDHrHMPDJ/fn3dEo6HoOra654PJ+zNwbTiMDvE5oAg/PLtnsUw==} engines: {node: '>=18'} - '@vitest/coverage-v8@3.2.6': - resolution: {integrity: sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==} + '@vitest/coverage-v8@4.1.10': + resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} peerDependencies: - '@vitest/browser': 3.2.6 - vitest: 3.2.6 + '@vitest/browser': 4.1.10 + vitest: 4.1.10 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@3.2.6': - resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/mocker@3.2.6': - resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==} + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@3.2.6': - resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/runner@3.2.6': - resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/snapshot@3.2.6': - resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/spy@3.2.6': - resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/ui@3.2.6': - resolution: {integrity: sha512-mATfG3zVdhobE9U1rIpvtYD3DGuSSxqZ3Aj/8ityGqKXy8YDJ9BoAjZmAz6dZ1IZ1xI5V+MerkCczvVa+3QK9Q==} + '@vitest/ui@4.1.10': + resolution: {integrity: sha512-EOUqfXHTXtpSHsyLHH40ts3Ue+hRhSGwzwzMlK0dTEOLSDYyOXLyr5JDGmHQWhN2DYI30gw6dVx3cdgM9FZl+Q==} peerDependencies: - vitest: 3.2.6 + vitest: 4.1.10 - '@vitest/utils@3.2.6': - resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} '@vue/compiler-core@3.5.24': resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==} @@ -3856,8 +3704,8 @@ packages: resolution: {integrity: sha512-WHw67kLXYbZuHTmcdbIrVArCq5wxo6NEuj3hiYAWr8mwJeC+C2mMCIBIWCiDoCye/OF/xelc+teJ1ERoWmnEIA==} engines: {node: '>=18'} - ast-v8-to-istanbul@0.3.8: - resolution: {integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==} + ast-v8-to-istanbul@1.0.5: + resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} @@ -4069,10 +3917,6 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - cacheable-lookup@6.1.0: resolution: {integrity: sha512-KJ/Dmo1lDDhmW2XDPMo+9oiy/CeqosPguPCrgcVzKyZrL6pM1gU2GmPY/xo6OQPTUaA/c0kwHuywB4E6nmT9ww==} engines: {node: '>=10.6.0'} @@ -4130,8 +3974,8 @@ packages: caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} chalk@2.4.2: @@ -4159,10 +4003,6 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} - check-error@2.1.3: - resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} - engines: {node: '>= 16'} - chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} @@ -4210,6 +4050,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + engines: {node: '>=0.10.0'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -4526,10 +4370,6 @@ packages: babel-plugin-macros: optional: true - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -4564,6 +4404,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -4592,11 +4436,6 @@ packages: resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==} engines: {node: '>=12.20'} - detect-libc@1.0.3: - resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} - engines: {node: '>=0.10'} - hasBin: true - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -4715,8 +4554,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - effect@3.21.0: - resolution: {integrity: sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==} + effect@4.0.0-beta.103: + resolution: {integrity: sha512-pE8TxF4m2tQzVI+77dIlm3s+81TACV1AiX1JEkvY+zVuxgQQ8aGSkqXNJF6b/ST+coCSk5cUdbULpQ7sm4oHyw==} ejs@5.0.1: resolution: {integrity: sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==} @@ -4798,6 +4637,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -5023,8 +4865,8 @@ packages: resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} engines: {node: '>=0.10.0'} - expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} expect@30.2.0: @@ -5064,14 +4906,14 @@ packages: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} - fast-check@3.23.2: - resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} - engines: {node: '>=8.0.0'} - fast-check@4.7.0: resolution: {integrity: sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==} engines: {node: '>=12.17.0'} + fast-check@4.9.0: + resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} + engines: {node: '>=12.17.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -5632,6 +5474,10 @@ packages: resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ini@7.0.0: + resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + inquirer@8.2.5: resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} engines: {node: '>=12.0.0'} @@ -5651,6 +5497,10 @@ packages: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} + engines: {node: '>=12.22.0'} + ip-regex@4.3.0: resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} engines: {node: '>=8'} @@ -6032,12 +5882,12 @@ packages: jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -6138,6 +5988,9 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + kubernetes-types@1.30.0: + resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -6240,9 +6093,6 @@ packages: resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==} engines: {node: '>=0.10.0'} - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lowdb@1.0.0: resolution: {integrity: sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==} engines: {node: '>=4'} @@ -6289,8 +6139,8 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magicast@0.3.5: - resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + magicast@0.5.4: + resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} make-asynchronous@1.1.0: resolution: {integrity: sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==} @@ -6388,6 +6238,11 @@ packages: engines: {node: '>=10.0.0'} hasBin: true + mime@4.1.0: + resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} + engines: {node: '>=16'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -6479,12 +6334,12 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msgpackr-extract@3.0.3: - resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + msgpackr-extract@3.0.4: + resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} hasBin: true - msgpackr@1.11.5: - resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} + msgpackr@2.0.5: + resolution: {integrity: sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==} msw@2.12.1: resolution: {integrity: sha512-arzsi9IZjjByiEw21gSUP82qHM8zkV69nNpWV6W4z72KiLvsDWoOp678ORV6cNfU/JGhlX0SsnD4oXo9gI6I2A==} @@ -6496,8 +6351,8 @@ packages: typescript: optional: true - multipasta@0.2.7: - resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} + multipasta@0.2.8: + resolution: {integrity: sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==} mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -6546,9 +6401,6 @@ packages: nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-fetch@2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} engines: {node: 4.x || >=6.0.0} @@ -6653,6 +6505,10 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -6859,10 +6715,6 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - peek-stream@1.1.3: resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} @@ -7027,9 +6879,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} @@ -7110,6 +6959,14 @@ packages: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + redux-thunk@3.1.0: resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} peerDependencies: @@ -7497,6 +7354,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -7505,8 +7365,8 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} steno@0.4.4: resolution: {integrity: sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w==} @@ -7604,9 +7464,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} @@ -7768,10 +7625,6 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - test-exclude@7.0.1: - resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} - engines: {node: '>=18'} - text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} @@ -7795,9 +7648,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -7806,16 +7656,8 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} tldts-core@6.1.86: @@ -7855,8 +7697,9 @@ packages: resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==} engines: {node: '>=14.16'} - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + toml@4.3.0: + resolution: {integrity: sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==} + engines: {node: '>=20'} totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} @@ -8052,9 +7895,9 @@ packages: resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==} engines: {node: '>=18.17'} - undici@7.28.0: - resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} - engines: {node: '>=20.18.1'} + undici@8.10.0: + resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} + engines: {node: '>=22.19.0'} unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} @@ -8131,14 +7974,14 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - uuid@11.1.1: - resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} - hasBin: true - uuid@14.0.0: resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true + uuid@14.0.1: + resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). @@ -8187,11 +8030,6 @@ packages: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - vite@7.3.6: resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -8232,31 +8070,44 @@ packages: yaml: optional: true - vitest-canvas-mock@1.1.3: - resolution: {integrity: sha512-zlKJR776Qgd+bcACPh0Pq5MG3xWq+CdkACKY/wX4Jyija0BSz8LH3aCCgwFKYFwtm565+050YFEGG9Ki0gE/Hw==} + vitest-canvas-mock@1.1.4: + resolution: {integrity: sha512-4boWHY+STwAxGl1+uwakNNoQky5EjPLC8HuponXNoAscYyT1h/F7RUvTkl4IyF/MiWr3V8Q626je3Iel3eArqA==} peerDependencies: vitest: ^3.0.0 || ^4.0.0 - vitest@3.2.6: - resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.6 - '@vitest/ui': 3.2.6 + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true - '@types/debug': + '@opentelemetry/api': optional: true '@types/node': optional: true - '@vitest/browser': + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': optional: true '@vitest/ui': optional: true @@ -8487,11 +8338,6 @@ snapshots: '@altano/repository-tools@2.0.3': {} - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@asamuzakjp/css-color@4.1.2': dependencies: '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) @@ -8647,8 +8493,12 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helper-wrap-function@7.28.3': @@ -8668,6 +8518,10 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -9292,6 +9146,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@bcoe/v8-coverage@0.2.3': {} '@bcoe/v8-coverage@1.0.2': {} @@ -9631,39 +9490,15 @@ snapshots: gonzales-pe: 4.3.0 node-source-walk: 7.0.1 - '@effect/cli@0.69.2(@effect/platform@0.90.10(effect@3.21.0))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/platform': 0.90.10(effect@3.21.0) - '@effect/printer': 0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0) - '@effect/printer-ansi': 0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0) - effect: 3.21.0 - ini: 4.1.3 - toml: 3.0.0 - yaml: 2.9.0 - - '@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/platform': 0.90.10(effect@3.21.0) - '@effect/rpc': 0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) - '@effect/sql': 0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) - '@effect/workflow': 0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - effect: 3.21.0 + '@effect/language-service@0.87.1': {} - '@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0)': + '@effect/opentelemetry@4.0.0-beta.103(@opentelemetry/api-logs@0.207.0)(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(effect@4.0.0-beta.103)': dependencies: - '@effect/platform': 0.90.10(effect@3.21.0) - effect: 3.21.0 - uuid: 11.1.1 - - '@effect/language-service@0.35.2': {} - - '@effect/opentelemetry@0.56.6(@effect/platform@0.90.10(effect@3.21.0))(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(effect@3.21.0)': - dependencies: - '@effect/platform': 0.90.10(effect@3.21.0) '@opentelemetry/semantic-conventions': 1.38.0 - effect: 3.21.0 + effect: 4.0.0-beta.103 optionalDependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.207.0 '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-logs': 0.207.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) @@ -9671,79 +9506,30 @@ snapshots: '@opentelemetry/sdk-trace-node': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-web': 2.2.0(@opentelemetry/api@1.9.0) - '@effect/platform-node-shared@0.47.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': + '@effect/platform-node-shared@4.0.0-beta.103(effect@4.0.0-beta.103)': dependencies: - '@effect/cluster': 0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/platform': 0.90.10(effect@3.21.0) - '@effect/rpc': 0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) - '@effect/sql': 0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) - '@parcel/watcher': 2.5.1 - effect: 3.21.0 - multipasta: 0.2.7 + '@types/ws': 8.18.1 + effect: 4.0.0-beta.103 ws: 8.21.1 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': + '@effect/platform-node@4.0.0-beta.103(effect@4.0.0-beta.103)(ioredis@5.11.1)': dependencies: - '@effect/cluster': 0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/platform': 0.90.10(effect@3.21.0) - '@effect/platform-node-shared': 0.47.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/rpc': 0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) - '@effect/sql': 0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) - effect: 3.21.0 - mime: 3.0.0 - undici: 7.28.0 - ws: 8.21.1 + '@effect/platform-node-shared': 4.0.0-beta.103(effect@4.0.0-beta.103) + effect: 4.0.0-beta.103 + ioredis: 5.11.1 + mime: 4.1.0 + undici: 8.10.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform@0.90.10(effect@3.21.0)': - dependencies: - effect: 3.21.0 - find-my-way-ts: 0.1.6 - msgpackr: 1.11.5 - multipasta: 0.2.7 - - '@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0)': + '@effect/vitest@4.0.0-beta.103(effect@4.0.0-beta.103)(vitest@4.1.10)': dependencies: - '@effect/printer': 0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0) - '@effect/typeclass': 0.36.0(effect@3.21.0) - effect: 3.21.0 - - '@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/typeclass': 0.36.0(effect@3.21.0) - effect: 3.21.0 - - '@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/platform': 0.90.10(effect@3.21.0) - effect: 3.21.0 - - '@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/experimental': 0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) - '@effect/platform': 0.90.10(effect@3.21.0) - effect: 3.21.0 - uuid: 11.1.1 - - '@effect/typeclass@0.36.0(effect@3.21.0)': - dependencies: - effect: 3.21.0 - - '@effect/vitest@0.27.0(effect@3.21.0)(vitest@3.2.6)': - dependencies: - effect: 3.21.0 - vitest: 3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) - - '@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/platform': 0.90.10(effect@3.21.0) - '@effect/rpc': 0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) - effect: 3.21.0 + effect: 4.0.0-beta.103 + vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) '@emnapi/core@1.4.5': dependencies: @@ -9966,6 +9752,8 @@ snapshots: optionalDependencies: '@types/node': 24.9.2 + '@ioredis/commands@1.10.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -10218,22 +10006,22 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': optional: true - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': optional: true - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': optional: true - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': optional: true - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': optional: true - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': optional: true '@mswjs/interceptors@0.40.0': @@ -10614,11 +10402,11 @@ snapshots: - typescript - verdaccio - '@nx/vite@22.7.6(@babel/traverse@7.28.5)(@nx/eslint@22.7.6(03cdb71bd33024bc72407dcf2fd22241))(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@3.2.6)': + '@nx/vite@22.7.6(@babel/traverse@7.28.5)(@nx/eslint@22.7.6(03cdb71bd33024bc72407dcf2fd22241))(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@nx/devkit': 22.7.6(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))) '@nx/js': 22.7.6(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(verdaccio@6.5.2(typanion@3.14.0)) - '@nx/vitest': 22.7.6(@babel/traverse@7.28.5)(@nx/eslint@22.7.6(03cdb71bd33024bc72407dcf2fd22241))(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@3.2.6) + '@nx/vitest': 22.7.6(@babel/traverse@7.28.5)(@nx/eslint@22.7.6(03cdb71bd33024bc72407dcf2fd22241))(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10) '@phenomnomnominal/tsquery': 6.2.0(typescript@5.8.3) ajv: 8.20.0 enquirer: 2.3.6 @@ -10627,7 +10415,7 @@ snapshots: tsconfig-paths: 4.2.0 tslib: 2.8.1 vite: 7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) - vitest: 3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) + vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - '@babel/traverse' - '@nx/eslint' @@ -10639,7 +10427,7 @@ snapshots: - typescript - verdaccio - '@nx/vitest@22.7.6(@babel/traverse@7.28.5)(@nx/eslint@22.7.6(03cdb71bd33024bc72407dcf2fd22241))(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@3.2.6)': + '@nx/vitest@22.7.6(@babel/traverse@7.28.5)(@nx/eslint@22.7.6(03cdb71bd33024bc72407dcf2fd22241))(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@nx/devkit': 22.7.6(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))) '@nx/js': 22.7.6(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(verdaccio@6.5.2(typanion@3.14.0)) @@ -10649,7 +10437,7 @@ snapshots: optionalDependencies: '@nx/eslint': 22.7.6(03cdb71bd33024bc72407dcf2fd22241) vite: 7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) - vitest: 3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) + vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -10660,7 +10448,7 @@ snapshots: - typescript - verdaccio - '@nx/web@22.7.6(04f16120cd0b4809abbf447c38c7f725)': + '@nx/web@22.7.6(294da7a21825b430d5914a528d2ef8ee)': dependencies: '@nx/devkit': 22.7.6(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))) '@nx/js': 22.7.6(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(verdaccio@6.5.2(typanion@3.14.0)) @@ -10672,7 +10460,7 @@ snapshots: '@nx/eslint': 22.7.6(03cdb71bd33024bc72407dcf2fd22241) '@nx/jest': 22.7.6(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(@types/node@24.9.2)(babel-plugin-macros@3.1.0)(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.21))(@types/node@24.9.2)(typescript@5.8.3))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0)) '@nx/playwright': 22.7.6(2adf0f06d3a79ef992d45b4845c9b15e) - '@nx/vite': 22.7.6(@babel/traverse@7.28.5)(@nx/eslint@22.7.6(03cdb71bd33024bc72407dcf2fd22241))(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@3.2.6) + '@nx/vite': 22.7.6(@babel/traverse@7.28.5)(@nx/eslint@22.7.6(03cdb71bd33024bc72407dcf2fd22241))(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.7.6(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -10904,66 +10692,6 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 - '@parcel/watcher-android-arm64@2.5.1': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.1': - optional: true - - '@parcel/watcher-darwin-x64@2.5.1': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.1': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.1': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.1': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.1': - optional: true - - '@parcel/watcher-win32-arm64@2.5.1': - optional: true - - '@parcel/watcher-win32-ia32@2.5.1': - optional: true - - '@parcel/watcher-win32-x64@2.5.1': - optional: true - - '@parcel/watcher@2.5.1': - dependencies: - detect-libc: 1.0.3 - is-glob: 4.0.3 - micromatch: 4.0.8 - node-addon-api: 7.1.1 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.1 - '@parcel/watcher-darwin-arm64': 2.5.1 - '@parcel/watcher-darwin-x64': 2.5.1 - '@parcel/watcher-freebsd-x64': 2.5.1 - '@parcel/watcher-linux-arm-glibc': 2.5.1 - '@parcel/watcher-linux-arm-musl': 2.5.1 - '@parcel/watcher-linux-arm64-glibc': 2.5.1 - '@parcel/watcher-linux-arm64-musl': 2.5.1 - '@parcel/watcher-linux-x64-glibc': 2.5.1 - '@parcel/watcher-linux-x64-musl': 2.5.1 - '@parcel/watcher-win32-arm64': 2.5.1 - '@parcel/watcher-win32-ia32': 2.5.1 - '@parcel/watcher-win32-x64': 2.5.1 - '@phenomnomnominal/tsquery@6.2.0(typescript@5.8.3)': dependencies: '@types/esquery': 1.5.4 @@ -11429,6 +11157,10 @@ snapshots: '@types/unist@3.0.3': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.9.2 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.34': @@ -11779,87 +11511,81 @@ snapshots: lodash: 4.18.1 minimatch: 7.4.9 - '@vitest/coverage-v8@3.2.6(vitest@3.2.6)': + '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': dependencies: - '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - ast-v8-to-istanbul: 0.3.8 - debug: 4.4.3 + '@vitest/utils': 4.1.10 + ast-v8-to-istanbul: 1.0.5 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 - magic-string: 0.30.21 - magicast: 0.3.5 - std-env: 3.10.0 - test-exclude: 7.0.1 - tinyrainbow: 2.0.0 - vitest: 3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) - transitivePeerDependencies: - - supports-color + magicast: 0.5.4 + obug: 2.1.4 + std-env: 4.2.0 + tinyrainbow: 3.1.1 + vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/expect@3.2.6': + '@vitest/expect@4.1.10': dependencies: + '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 3.2.6 - '@vitest/utils': 3.2.6 - chai: 5.3.3 - tinyrainbow: 2.0.0 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.1 - '@vitest/mocker@3.2.6(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@vitest/spy': 3.2.6 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.1(@types/node@24.9.2)(typescript@5.8.3) vite: 7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) - '@vitest/mocker@3.2.6(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@vitest/spy': 3.2.6 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.1(@types/node@24.9.2)(typescript@5.9.3) vite: 7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) - '@vitest/pretty-format@3.2.6': + '@vitest/pretty-format@4.1.10': dependencies: - tinyrainbow: 2.0.0 + tinyrainbow: 3.1.1 - '@vitest/runner@3.2.6': + '@vitest/runner@4.1.10': dependencies: - '@vitest/utils': 3.2.6 + '@vitest/utils': 4.1.10 pathe: 2.0.3 - strip-literal: 3.1.0 - '@vitest/snapshot@3.2.6': + '@vitest/snapshot@4.1.10': dependencies: - '@vitest/pretty-format': 3.2.6 + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@3.2.6': - dependencies: - tinyspy: 4.0.4 + '@vitest/spy@4.1.10': {} - '@vitest/ui@3.2.6(vitest@3.2.6)': + '@vitest/ui@4.1.10(vitest@4.1.10)': dependencies: - '@vitest/utils': 3.2.6 + '@vitest/utils': 4.1.10 fflate: 0.8.2 flatted: 3.4.2 pathe: 2.0.3 sirv: 3.0.2 tinyglobby: 0.2.15 - tinyrainbow: 2.0.0 - vitest: 3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) + tinyrainbow: 3.1.1 + vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/utils@3.2.6': + '@vitest/utils@4.1.10': dependencies: - '@vitest/pretty-format': 3.2.6 - loupe: 3.2.1 - tinyrainbow: 2.0.0 + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 '@vue/compiler-core@3.5.24': dependencies: @@ -12274,11 +12000,11 @@ snapshots: ast-module-types@6.0.1: {} - ast-v8-to-istanbul@0.3.8: + ast-v8-to-istanbul@1.0.5: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 - js-tokens: 9.0.1 + js-tokens: 10.0.0 async-function@1.0.0: {} @@ -12539,8 +12265,6 @@ snapshots: bytes@3.1.2: {} - cac@6.7.14: {} - cacheable-lookup@6.1.0: {} cacheable-lookup@7.0.0: {} @@ -12598,13 +12322,7 @@ snapshots: caseless@0.12.0: {} - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.3 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 + chai@6.2.2: {} chalk@2.4.2: dependencies: @@ -12627,8 +12345,6 @@ snapshots: chardet@2.1.1: {} - check-error@2.1.3: {} - chrome-trace-event@1.0.4: {} ci-info@4.3.1: {} @@ -12663,6 +12379,8 @@ snapshots: clone@1.0.4: {} + cluster-key-slot@1.1.1: {} + co@4.6.0: {} code-block-writer@13.0.3: {} @@ -12959,8 +12677,6 @@ snapshots: optionalDependencies: babel-plugin-macros: 3.1.0 - deep-eql@5.0.2: {} - deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -12989,6 +12705,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} dependency-tree@11.2.0: @@ -13010,8 +12728,6 @@ snapshots: detect-indent@7.0.2: {} - detect-libc@1.0.3: {} - detect-libc@2.1.2: optional: true @@ -13136,10 +12852,18 @@ snapshots: ee-first@1.1.1: {} - effect@3.21.0: + effect@4.0.0-beta.103: dependencies: '@standard-schema/spec': 1.1.0 - fast-check: 3.23.2 + fast-check: 4.9.0 + find-my-way-ts: 0.1.6 + ini: 7.0.0 + kubernetes-types: 1.30.0 + msgpackr: 2.0.5 + multipasta: 0.2.8 + toml: 4.3.0 + uuid: 14.0.1 + yaml: 2.9.0 ejs@5.0.1: {} @@ -13253,6 +12977,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.3.1: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -13556,7 +13282,7 @@ snapshots: dependencies: homedir-polyfill: 1.0.3 - expect-type@1.2.2: {} + expect-type@1.4.0: {} expect@30.2.0: dependencies: @@ -13659,11 +13385,11 @@ snapshots: extsprintf@1.3.0: {} - fast-check@3.23.2: + fast-check@4.7.0: dependencies: - pure-rand: 6.1.0 + pure-rand: 8.4.0 - fast-check@4.7.0: + fast-check@4.9.0: dependencies: pure-rand: 8.4.0 @@ -14293,6 +14019,8 @@ snapshots: ini@4.1.3: {} + ini@7.0.0: {} + inquirer@8.2.5: dependencies: ansi-escapes: 4.3.2 @@ -14340,6 +14068,18 @@ snapshots: interpret@1.4.0: {} + ioredis@5.11.1: + dependencies: + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-regex@4.3.0: {} ipaddr.js@1.9.1: {} @@ -14852,9 +14592,9 @@ snapshots: jose@6.2.3: {} - js-tokens@4.0.0: {} + js-tokens@10.0.0: {} - js-tokens@9.0.1: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: dependencies: @@ -14984,6 +14724,8 @@ snapshots: kind-of@6.0.3: {} + kubernetes-types@1.30.0: {} + leven@3.1.0: {} levn@0.4.1: @@ -15062,8 +14804,6 @@ snapshots: longest@2.0.1: {} - loupe@3.2.1: {} - lowdb@1.0.0: dependencies: graceful-fs: 4.2.11 @@ -15115,10 +14855,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - magicast@0.3.5: + magicast@0.5.4: dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 source-map-js: 1.2.1 make-asynchronous@1.1.0: @@ -15193,6 +14933,8 @@ snapshots: mime@3.0.0: {} + mime@4.1.0: {} + mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} @@ -15268,21 +15010,21 @@ snapshots: ms@2.1.3: {} - msgpackr-extract@3.0.3: + msgpackr-extract@3.0.4: dependencies: node-gyp-build-optional-packages: 5.2.2 optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 optional: true - msgpackr@1.11.5: + msgpackr@2.0.5: optionalDependencies: - msgpackr-extract: 3.0.3 + msgpackr-extract: 3.0.4 msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3): dependencies: @@ -15335,7 +15077,7 @@ snapshots: transitivePeerDependencies: - '@types/node' - multipasta@0.2.7: {} + multipasta@0.2.8: {} mute-stream@0.0.8: {} @@ -15361,8 +15103,6 @@ snapshots: nice-try@1.0.5: {} - node-addon-api@7.1.1: {} - node-fetch@2.6.7: dependencies: whatwg-url: 5.0.0 @@ -15577,6 +15317,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.4: {} + on-exit-leak-free@2.1.2: {} on-finished@2.4.1: @@ -15788,8 +15530,6 @@ snapshots: pathe@2.0.3: {} - pathval@2.0.1: {} - peek-stream@1.1.3: dependencies: buffer-from: 1.1.2 @@ -15971,8 +15711,6 @@ snapshots: punycode@2.3.1: {} - pure-rand@6.1.0: {} - pure-rand@7.0.1: {} pure-rand@8.4.0: {} @@ -16071,6 +15809,12 @@ snapshots: dependencies: resolve: 1.22.11 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + redux-thunk@3.1.0(redux@5.0.1): dependencies: redux: 5.0.1 @@ -16533,11 +16277,13 @@ snapshots: stackback@0.0.2: {} + standard-as-callback@2.1.0: {} + statuses@2.0.1: {} statuses@2.0.2: {} - std-env@3.10.0: {} + std-env@4.2.0: {} steno@0.4.4: dependencies: @@ -16648,10 +16394,6 @@ snapshots: strip-json-comments@3.1.1: {} - strip-literal@3.1.0: - dependencies: - js-tokens: 9.0.1 - strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 @@ -16793,12 +16535,6 @@ snapshots: glob: 7.2.3 minimatch: 3.1.5 - test-exclude@7.0.1: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 10.4.5 - minimatch: 9.0.9 - text-decoder@1.2.3: dependencies: b4a: 1.7.3 @@ -16824,8 +16560,6 @@ snapshots: tinybench@2.9.0: {} - tinyexec@0.3.2: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -16833,11 +16567,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tinypool@1.1.1: {} - - tinyrainbow@2.0.0: {} - - tinyspy@4.0.4: {} + tinyrainbow@3.1.1: {} tldts-core@6.1.86: {} @@ -16871,7 +16601,7 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - toml@3.0.0: {} + toml@4.3.0: {} totalist@3.0.1: {} @@ -17088,7 +16818,7 @@ snapshots: undici@6.25.0: {} - undici@7.28.0: {} + undici@8.10.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -17163,10 +16893,10 @@ snapshots: utils-merge@1.0.1: {} - uuid@11.1.1: {} - uuid@14.0.0: {} + uuid@14.0.1: {} + uuid@8.3.2: {} v8-compile-cache-lib@3.0.1: {} @@ -17259,27 +16989,6 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 - vite-node@3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.27.2 @@ -17296,97 +17005,73 @@ snapshots: tsx: 4.21.0 yaml: 2.9.0 - vitest-canvas-mock@1.1.3(vitest@3.2.6): + vitest-canvas-mock@1.1.4(vitest@4.1.10): dependencies: cssfontparser: 1.2.1 moo-color: 1.0.3 - vitest: 3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) - - vitest@3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0): - dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.6 - '@vitest/mocker': 3.2.6(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/pretty-format': 3.2.6 - '@vitest/runner': 3.2.6 - '@vitest/snapshot': 3.2.6 - '@vitest/spy': 3.2.6 - '@vitest/utils': 3.2.6 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.2.2 + vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) + + vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 magic-string: 0.30.21 + obug: 2.1.4 pathe: 2.0.3 picomatch: 4.0.4 - std-env: 3.10.0 + std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 0.3.2 + tinyexec: 1.0.2 tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 + tinyrainbow: 3.1.1 vite: 7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/node': 24.9.2 - '@vitest/ui': 3.2.6(vitest@3.2.6) + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + '@vitest/ui': 4.1.10(vitest@4.1.10) jsdom: 27.4.0(@noble/hashes@1.8.0) transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vitest@3.2.6(@types/node@24.9.2)(@vitest/ui@3.2.6)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0): - dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.6 - '@vitest/mocker': 3.2.6(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/pretty-format': 3.2.6 - '@vitest/runner': 3.2.6 - '@vitest/snapshot': 3.2.6 - '@vitest/spy': 3.2.6 - '@vitest/utils': 3.2.6 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.2.2 + vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 magic-string: 0.30.21 + obug: 2.1.4 pathe: 2.0.3 picomatch: 4.0.4 - std-env: 3.10.0 + std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 0.3.2 + tinyexec: 1.0.2 tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 + tinyrainbow: 3.1.1 vite: 7.3.6(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/node': 24.9.2 - '@vitest/ui': 3.2.6(vitest@3.2.6) + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + '@vitest/ui': 4.1.10(vitest@4.1.10) jsdom: 27.4.0(@noble/hashes@1.8.0) transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml w3c-xmlserializer@5.0.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d00cd28562..1e9c1073e3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -17,17 +17,16 @@ catalog: tsx: '4.21.0' catalogs: effect: - effect: '^3.20.0' - '@effect/cli': '^0.69.0' - '@effect/language-service': '^0.35.2' - '@effect/platform': '^0.90.0' - '@effect/platform-node': '0.94.2' - '@effect/vitest': '^0.27.0' - '@effect/opentelemetry': '^0.56.1' + effect: '4.0.0-beta.103' + '@effect/language-service': '0.87.1' + '@effect/platform-node': '4.0.0-beta.103' + '@effect/vitest': '4.0.0-beta.103' + '@effect/opentelemetry': '4.0.0-beta.103' vitest: - vitest: '^3.2.6' - '@vitest/coverage-v8': '^3.2.6' - 'vitest-canvas-mock': '^1.1.3' + vitest: '^4.1.0' + '@vitest/coverage-v8': '^4.1.0' + '@vitest/ui': '^4.1.0' + 'vitest-canvas-mock': '^1.1.4' vite: vite: '^7.3.6' diff --git a/tools/release/package.json b/tools/release/package.json index 5925c21ecb..7cebf9aef2 100644 --- a/tools/release/package.json +++ b/tools/release/package.json @@ -9,7 +9,6 @@ "main": "index.js", "scripts": {}, "dependencies": { - "@effect/platform": "catalog:effect", "@effect/platform-node": "catalog:effect", "effect": "catalog:effect" } diff --git a/tools/user-scripts/package.json b/tools/user-scripts/package.json index cd9a532cab..6f2ca980c2 100644 --- a/tools/user-scripts/package.json +++ b/tools/user-scripts/package.json @@ -16,7 +16,6 @@ "test:watch": "pnpm nx nxTest --watch" }, "dependencies": { - "@effect/platform": "catalog:effect", "@effect/platform-node": "catalog:effect", "effect": "catalog:effect", "vitest": "catalog:vitest"