Skip to content

feat(dart): Dart/Flutter SDK parity with TypeScript — morph_core, morph_oauth2, morph_logger, morph_storage + Flutter POC#37

Open
cemal-yilmaz-bt wants to merge 35 commits into
masterfrom
f/plugin
Open

feat(dart): Dart/Flutter SDK parity with TypeScript — morph_core, morph_oauth2, morph_logger, morph_storage + Flutter POC#37
cemal-yilmaz-bt wants to merge 35 commits into
masterfrom
f/plugin

Conversation

@cemal-yilmaz-bt

Copy link
Copy Markdown

Overview

This PR merges 33 commits from f/plugin into master, delivering a near-complete Dart/Flutter port of the TypeScript Morph API Client SDK, validated end-to-end with a Flutter POC app against a real Keycloak instance.


What was built

packages/dart/morph_core — Core SDK (parity with @morph/core)

Module TS reference Status
Types & config schema types.ts ✅ Full parity — MorphConfig, AuthContextConfig, ProviderConfig, HostConfig, MorphOptions, TokenSet, OAuthReturnResult, MorphContextMeta, MorphProviderMeta, etc.
Config validation + indexing config/validate.ts validateAndIndexConfigResolvedMorphConfig
Config interpolation config/interpolate.ts interpolateString
Runtime / coordinator runtime.ts ✅ All methods: getAuthorizationUrl, getExchangeSources, getExchangeTargets, getTokenStatus, completeOAuthCallback, completeOAuthReturn, isAuthContextReady, isProviderEnvReady
Host pipeline http/hostPipeline.ts hostFetch, 401 recovery, timeout, HTTP trace emission
MorphClient facade client/MorphClient.ts ✅ All public methods exposed with identical signatures
AuthHandle client/AuthHandle.ts submitCode, acquireWithClientCredentials, exchangeToken, setTokens, clearTokens, logout, hasValidToken, refreshTokens, peekTokens, getClaims
HostClient client/HostClient.ts get, post, put, patch, delete, request
Plugin system runtime.ts (topoSort + install) plugin_install.dart — topological sort, MorphPluginContext, provideAuth, provideStorage
All utils util/*.ts ✅ JWT decode, expiry, OAuth authorize/return/state, normalize origin, exchange sources, list auth IDs, HTTP trace headers

packages/dart/morph_oauth2 — OAuth2 plugin (parity with @morph/oauth2)

Module Status
TokenLifecycle ✅ All 13 methods: loadTokens, submitCode, acquireWithClientCredentials, exchangeToken, setTokens, clearTokens, logout, logoutProvider, hasValidTokenContext, hasValidTokenProvider, handle401Recovery, resolveAccessToken, refreshTokensManual
TokenVault ✅ Storage I/O with StorageConfig scoping
TokenHttp ✅ Token endpoint HTTP — client creds, auth code, refresh, token exchange
oauth2Plugin MorphPlugin wrapper — topological install, AuthPlugin provision

packages/dart/morph_logger — Logger plugin (parity with @morph/logger)

loggerPlugin, createLogger, LoggerPluginOptions (level, prefix, onLog, onHttpTrace, httpTrace flag), morphDefaultHttpTrace, morphHttpTraceMessage

packages/dart/morph_storage — Storage plugin

memoryStorageMorphPlugin — in-memory StorageProvider for tests and VM targets.

Note: Persistent storage adapters (flutter_secure_storage, shared_preferences) are not yet implemented — tracked in issue #21. The memory plugin is sufficient for the POC and unit tests.

poc/flutter-poc — Flutter POC app

✅ End-to-end proof of concept against real Keycloak:

  • Full web OAuth2 flow (authorize redirect → code exchange → token storage)
  • Simulation engine (PocSimStep sealed classes, runPocSimStep, isPocSessionDeadStop)
  • 15 unit tests (poc_simulation_test.dart)
  • Web-specific fixes: CORS webOrigins, profile-mode runner, memoryStorageMorphPlugin on web to avoid ContextStore identity deadlock on redirect

Parity status

Area Parity
Config schema + validation ✅ 100%
Runtime + OAuth2 flow ✅ 100%
HTTP pipeline + trace ✅ 100%
MorphClient public API ✅ 100% — all methods including getAuthorizationUrl, getExchangeSources, getExchangeTargets
AuthHandle ✅ 100% — all 10 methods
Logger plugin ✅ 100%
Storage — persistent adapters ❌ In-memory only (issue #21 — deferred)

Known open issues (all low priority)

# Title Notes
#22 AuthHandle double-parse per call Same pattern exists in TS — at parity
#21 morph_storage CI matrix Waiting on persistent adapter + standalone tests
#19 Uri.base.origin crash on file:// URIs Half-guarded — VM/desktop edge case
#18 dart-parity.md concrete file pointers Docs cleanup

Test coverage

packages/dart/morph_core/test/
  config_and_morph_client_test.dart    — config validation, MorphClient init
  morph_client_bootstrap_test.dart     — plugin install, dispose
  morph_client_facade_test.dart        — public API surface, host/auth lookup, OAuth return
  oauth_runtime_return_test.dart       — OAuth callback handling
  plugin_install_test.dart             — topological sort, cycle detection

packages/dart/morph_oauth2/test/
  oauth2_plugin_install_test.dart      — plugin install, AuthPlugin provision

packages/dart/morph_logger/test/
  logger_plugin_test.dart              — log level filtering, HTTP trace chaining

poc/flutter-poc/test/
  poc_simulation_test.dart             — 15 tests: step models, JSON parsing, session guard, mocked HTTP

What's next (post-merge)

  1. Persistent storage adapterflutter_secure_storage-backed morph_storage plugin for mobile (issue [priority: low] CI: add morph_storage to Dart workflow matrix when tests land #21)
  2. Uri.base.origin hardening — guard file:// URI case (issue [priority: low] morph_core: OAuth redirect resolver hardening (Uri.origin / file URIs) #19)
  3. morph_storage CI matrix — add to dart.yml once tests land (issue [priority: low] CI: add morph_storage to Dart workflow matrix when tests land #21)

🤖 Generated with Claude Code

middt and others added 30 commits March 24, 2026 16:59
- Add packages/dart/morph_core with stub MorphClient.init (UnimplementedError)
- Document Dart parity roadmap in docs/dart-parity.md
- GitHub Actions: dart analyze + dart test
- Makefile: dart-get, dart-analyze, dart-test, dart-all
- Issue body template at docs/github-issue-1-body.md for GitHub #1

Made-with: Cursor
feat(dart): morph_core scaffold, CI, and dart-parity docs
- Add validateAndIndexConfig parity with packages/core/src/config/validate.ts
- ConfigValidationError, CtxRef, ResolvedMorphConfig, normalizeExchangeSources,
  listAuthIdsForProvider
- MorphClient.init validates JSON map then UnimplementedError
- Tests + docs/dart-parity.md + README updates

Made-with: Cursor
feat(dart): Morph config validation parity with TS
- Add TS-parity typed models (MorphConfig, providers/contexts/hosts),
  ResolvedMorphConfig with typed indexes, CtxRef.
- validateAndIndexConfig accepts MorphConfig or JSON Map; interpolate + full
  morph errors export.
- Port duration/url/jwt/oauth helpers + expiry parity builders.
- Add HostPipeline (http package) aligned with TS hostPipeline.
- New packages morph_logger (createLogger), morph_storage (MemoryStorageProvider).

OAuth2 plugin/token lifecycle, plugin runtime/MorphRuntime, HostClient/AuthHandle,
and full MorphClient wiring remain TODO.

Made-with: Cursor
feat(dart): typed config, utils, HostPipeline, morph_logger, morph_storage
- Add morph_oauth2 package (token HTTP helpers, vault, lifecycle AuthPlugin impl)
- Add oauth2Plugin() with MorphOAuthCallbacksPartial and storage/logger resolution
- Add memoryStorageMorphPlugin to morph_storage for tests and CI
- Extend Dart CI workflow to matrix morph_core and morph_oauth2

Refs #6

Made-with: Cursor
feat(dart): morph_oauth2 parity (TokenVault, TokenLifecycle, oauth2Plugin)
- Add LoggerPluginOptions, createLogger -> MorphLogFn, morphHttpTraceMessage
- Add loggerPlugin MorphPlugin (@morph/logger): chain onLog / onHttpTrace
- Depend on morph_core; add unit tests; run morph_logger in CI matrix

Closes #8
Refs #3

Made-with: Cursor
feat(dart): morph_logger LoggerPluginOptions + loggerPlugin parity
- Add topoSortPlugins + installMorphPlugins (parity packages/core/src/runtime.ts)
- Add MorphRuntime (tokens, storage, HostPipeline, config/token/OAuth helpers)
- MorphClient.init validates config and boots runtime; expose .runtime
- Export HostPipeline; validate unsatisfied plugin requires even for a single plugin
- dev_deps: morph_oauth2 + morph_storage for bootstrap integration test

Closes #10
Refs #3

Co-authored-by: Cursor <cursoragent@cursor.com>
- Drop split('.').length JWT probe; decode via try/catch like access branch
- hasRefreshToken: set?.refreshToken?.isNotEmpty ?? false

Co-authored-by: Cursor <cursoragent@cursor.com>
…trap

feat(dart): MorphRuntime + MorphClient bootstrap (plugin install parity)
Closes #12

Co-authored-by: u0b002 <cyilmaz@burgan.com.tr>
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add MorphOptions.oauthRedirectBase for root callback redirect Uri
- completeOAuthReturn({Uri? currentUri}) + conditional browser helpers
- Tests on MorphRuntime OAuth return parsing

Closes #13

Co-authored-by: u0b002 <cyilmaz@burgan.com.tr>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(dart): OAuth redirect base and completeOAuthReturn parity

- Add MorphOptions.oauthRedirectBase for root callback redirect Uri
- completeOAuthReturn({Uri? currentUri}) + conditional browser helpers
- Tests on MorphRuntime OAuth return parsing

Closes #13

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(dart): MorphClient + HostClient + AuthHandle façade parity

- Add HostClient and AuthHandle mirroring `@morph/core` clients
- Extend MorphClient with TS-equivalent façade methods and exports

Closes #14

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: u0b002 <cyilmaz@burgan.com.tr>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(poc): add Flutter sample app (poc/flutter-poc) — closes #24

Adds a Flutter PoC that mirrors poc/ts-vue against the same
Keycloak + mock-api backend:

- lib/morph_init.dart  — MorphClient init with loggerPlugin,
  oauth2Plugin, memoryStorageMorphPlugin and an onHttpTrace collector
- lib/main.dart        — MaterialApp + app_links deep-link listener
  for morphpoc://oauth/callback
- lib/screens/home_screen.dart — token status cards, action buttons
  (acquire device token, login, exchange, logout), mock API call,
  HTTP trace log panel
- lib/widgets/token_status_card.dart — per-authId card + JWT claims sheet
- lib/widgets/http_trace_log.dart   — expandable MorphHttpTraceEvent log
- Android morphpoc:// intent-filter; iOS CFBundleURLSchemes
- assets/poc-config.json (copy of docs/poc/poc-config.json)
- README.md with prereqs, run steps, OAuth flow, feature parity table

docs/dart-parity.md: marks "Sample app" row as Done.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(poc/flutter): address Gemini review comments

- http_trace_log: rewrite _DetailPanel content switch as a Dart 3
  switch expression (cleaner exhaustiveness, no ambiguity)
- main.dart: store StreamSubscription and cancel it in dispose()
  to prevent deep-link listener leak (dart:async import added)
- morph_init.dart: detect Platform.isAndroid and replace localhost
  with 10.0.2.2 so the app works on Android emulators
- http_trace_log: add comment explaining shrinkWrap tradeoff
- README: document Android emulator networking (10.0.2.2)

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: u0b002 <cyilmaz@burgan.com.tr>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(poc): switch flutter-poc to persistent ContextStore storage

Replaces the in-memory memoryStorageMorphPlugin with the new
morph_core_storage adapter backed by morph_data_store's ContextStore.
Tokens now survive app restarts via platform Keychain / KeyStore.

- pubspec.yaml: add morph_core_storage + morph_data_store path deps
- morph_init.dart: create ContextStore, wire contextStoreStoragePlugin
  before oauth2Plugin; remove morph_storage import
- docs/dart-parity.md: mark "Persistent / browser storage" as Done

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(poc): address Gemini review comments on storage init

- Extract _appendLog helper: consistent [$level] format used by both
  MorphOptions.onLog and ContextStoreOptions.onLog; eliminates duplication
- Wrap ContextStore.create in try/catch: falls back to in-memory storage
  (memoryStorageMorphPlugin) if Keychain is unavailable (e.g. simulator
  without entitlements); logs a warn entry either way
- Restore morph_storage dep: required for the in-memory fallback path
- Comment: note that onRequestServerTime returns null (PoC tradeoff;
  ContextStore falls back to device time per grand-rules §9)
- Note: cross-repo path: deps are expected for this local-workspace PoC;
  publish_to: "none" moratorium means no CI publish concern

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: u0b002 <cyilmaz@burgan.com.tr>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(poc): Flutter POC feature parity with TS Vue POC

Closes #27

- Add assets/poc-simulation.json (copy of docs/poc/poc-simulation.json)
- Add lib/poc_simulation.dart: PocSimStep sealed classes + runPocSimStep
  executor for fetch / host / logout_provider steps
- Add lib/widgets/mock_api_sheet.dart: bottom sheet with all 9 simulation
  steps as buttons + integrated HTTP trace log
- Add lib/widgets/provider_config_sheet.dart: shows getProviderMeta() as
  formatted JSON
- Add lib/widgets/simulation_panel.dart: sequential auto-loop runner with
  per-step result rows, 404-probe toggle, and session-dead guard
- Refactor home_screen.dart: status grouped by provider, per-row dynamic
  actions driven by grantHint, Config button, token exchange dropdown
- Add getMockApiBase() to morph_init.dart (Android 10.0.2.2 aware)
- Add http as direct dependency; fix morph_realm.json localhost:4200 URIs
- flutter analyze: no issues

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(poc): complete web OAuth callback before runApp to fix token visibility

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(poc): navigate current tab for web OAuth login instead of opening new tab

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-poc): switch to profile mode and extend OAuth code lifetime

Debug mode loads 596 separate JS files (30-60s) causing the Keycloak
authorization code (60s default) to expire before completeOAuthCallback
can exchange it, resulting in a blank screen / no-token state.

- run_web.sh: default to --profile (single bundled JS, ~2s load);
  --debug flag available when hot-reload is needed
- morph-realm.json: accessCodeLifespan 60s→300s so even slower loads
  won't race the code expiry window

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(web-poc): add missing web/ platform scaffold

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-poc): add CORS webOrigins for device+session clients, add SDK log panel

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-poc): route SDK logs to browser console, remove UI log panel

Co-authored-by: Cursor <cursoragent@cursor.com>

* debug(web-poc): add exhaustive tracing to catch profile-mode silent crashes

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-poc): use in-memory storage on web to avoid ContextStore identity deadlock

ContextStore.setData with Boundary.user silently drops writes when
_activeUser is not set. On web the OAuth redirect reloads the page so
the user identity is never established before the token write, causing
all session-scoped tokens to be silently discarded.

Use memoryStorageMorphPlugin on web: completeOAuthCallback() runs in
main() before runApp(), so the token is in the same JS heap the UI
then reads from — no persistence needed for the current page load.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-poc): render UI before processing OAuth callback to unblock blank screen

The blank screen was caused by completeOAuthCallback hanging or crashing
silently before runApp() was called.  Render the UI first, then process
the OAuth callback in initState via addPostFrameCallback.  HomeScreen
exposes a public refreshStatus() that the parent app calls after the
callback completes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(poc): address PR #28 review (precedence, timeouts, UI dupes)

- simulation_panel: fix &&/|| precedence in session-dead guard
- simulation_panel: drop duplicate Simulation title; use Spacer in toolbar
- mock_api_sheet: message color from result.isError; remove redundant ternary
- poc_simulation: 10s timeout on http.get; document PoC string-matching tradeoff

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: u0b002 <cyilmaz@burgan.com.tr>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(poc): Flutter POC feature parity with TS Vue POC

Closes #27

- Add assets/poc-simulation.json (copy of docs/poc/poc-simulation.json)
- Add lib/poc_simulation.dart: PocSimStep sealed classes + runPocSimStep
  executor for fetch / host / logout_provider steps
- Add lib/widgets/mock_api_sheet.dart: bottom sheet with all 9 simulation
  steps as buttons + integrated HTTP trace log
- Add lib/widgets/provider_config_sheet.dart: shows getProviderMeta() as
  formatted JSON
- Add lib/widgets/simulation_panel.dart: sequential auto-loop runner with
  per-step result rows, 404-probe toggle, and session-dead guard
- Refactor home_screen.dart: status grouped by provider, per-row dynamic
  actions driven by grantHint, Config button, token exchange dropdown
- Add getMockApiBase() to morph_init.dart (Android 10.0.2.2 aware)
- Add http as direct dependency; fix morph_realm.json localhost:4200 URIs
- flutter analyze: no issues

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(poc): complete web OAuth callback before runApp to fix token visibility

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(poc): navigate current tab for web OAuth login instead of opening new tab

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-poc): switch to profile mode and extend OAuth code lifetime

Debug mode loads 596 separate JS files (30-60s) causing the Keycloak
authorization code (60s default) to expire before completeOAuthCallback
can exchange it, resulting in a blank screen / no-token state.

- run_web.sh: default to --profile (single bundled JS, ~2s load);
  --debug flag available when hot-reload is needed
- morph-realm.json: accessCodeLifespan 60s→300s so even slower loads
  won't race the code expiry window

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(web-poc): add missing web/ platform scaffold

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-poc): add CORS webOrigins for device+session clients, add SDK log panel

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-poc): route SDK logs to browser console, remove UI log panel

Co-authored-by: Cursor <cursoragent@cursor.com>

* debug(web-poc): add exhaustive tracing to catch profile-mode silent crashes

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-poc): use in-memory storage on web to avoid ContextStore identity deadlock

ContextStore.setData with Boundary.user silently drops writes when
_activeUser is not set. On web the OAuth redirect reloads the page so
the user identity is never established before the token write, causing
all session-scoped tokens to be silently discarded.

Use memoryStorageMorphPlugin on web: completeOAuthCallback() runs in
main() before runApp(), so the token is in the same JS heap the UI
then reads from — no persistence needed for the current page load.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-poc): render UI before processing OAuth callback to unblock blank screen

The blank screen was caused by completeOAuthCallback hanging or crashing
silently before runApp() was called.  Render the UI first, then process
the OAuth callback in initState via addPostFrameCallback.  HomeScreen
exposes a public refreshStatus() that the parent app calls after the
callback completes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(poc): address PR #28 review (precedence, timeouts, UI dupes)

- simulation_panel: fix &&/|| precedence in session-dead guard
- simulation_panel: drop duplicate Simulation title; use Spacer in toolbar
- mock_api_sheet: message color from result.isError; remove redundant ternary
- poc_simulation: 10s timeout on http.get; document PoC string-matching tradeoff

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(poc): add unit tests for poc_simulation + injectable fetch client

- Extract parsePocSimulationJson for JSON parsing without rootBundle
- Add isPocSessionDeadStop (used by SimulationPanel) with regression tests
- Refactor _runFetch to optional http.Client override + runPocSimFetchForTesting
- Add 15 tests: models, parser, session guard, mocked fetch, asset load
- Replace empty widget_test placeholder with minimal smoke test

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: sync Dart/Flutter PoC parity, web/runtime notes, and test coverage

- dart-parity: Flutter PoC parity (#27/#28), web vs native storage, Keycloak
  webOrigins, run_web.sh profile default, unit tests, CI scope for flutter-poc
- README: dart-parity description reflects current status
- poc-guide: new Flutter PoC section (run paths, OAuth, CORS, simulation, tests)
- poc/simulation: Flutter executor + isPocSessionDeadStop + test pointer

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: u0b002 <cyilmaz@burgan.com.tr>
Co-authored-by: Cursor <cursoragent@cursor.com>
cemal-yilmaz-bt and others added 3 commits May 4, 2026 22:37
* chore: nest TypeScript SDK under packages/ts/

- Move core, oauth2, logger, browser-storage to packages/ts/ for parity with packages/dart/
- Point npm workspaces and Makefile/package scripts at packages/ts/*
- Update poc/ts-vue file: deps, README, dart-parity, architecture tree, Dart parity comments

Closes #31

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: align architecture project tree (Gemini review)

- Align branch comments and restore browserStorage.ts description
- Regenerate package-lock.json after clean npm install (remove extraneous entries)

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: u0b002 <cyilmaz@burgan.com.tr>
Co-authored-by: Cursor <cursoragent@cursor.com>
…+ reuse http.Client (#36)

- isPocSessionDeadStop: replace any() closure with type check + contains()
- SimulationPanel: shared http.Client for TCP keep-alive across sim steps
- runPocSimStep: optional http.Client? param threaded to _runFetch
@cemal-yilmaz-bt cemal-yilmaz-bt requested review from a team June 1, 2026 20:32

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @cemal-yilmaz-bt, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 193 files, which is 43 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a3fb1f7-150c-4f30-8b5c-26fdd93e3fd3

📥 Commits

Reviewing files that changed from the base of the PR and between 2803d6f and 6671603.

⛔ Files ignored due to path filters (35)
  • package-lock.json is excluded by !**/package-lock.json
  • packages/dart/morph_core/pubspec.lock is excluded by !**/*.lock
  • packages/dart/morph_logger/pubspec.lock is excluded by !**/*.lock
  • packages/dart/morph_oauth2/pubspec.lock is excluded by !**/*.lock
  • packages/dart/morph_storage/pubspec.lock is excluded by !**/*.lock
  • poc/flutter-poc/android/app/src/main/res/mipmap-hdpi/ic_launcher.png is excluded by !**/*.png
  • poc/flutter-poc/android/app/src/main/res/mipmap-mdpi/ic_launcher.png is excluded by !**/*.png
  • poc/flutter-poc/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png is excluded by !**/*.png
  • poc/flutter-poc/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png is excluded by !**/*.png
  • poc/flutter-poc/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png is excluded by !**/*.png
  • poc/flutter-poc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png is excluded by !**/*.png
  • poc/flutter-poc/pubspec.lock is excluded by !**/*.lock
  • poc/flutter-poc/web/favicon.png is excluded by !**/*.png
  • poc/flutter-poc/web/icons/Icon-192.png is excluded by !**/*.png
  • poc/flutter-poc/web/icons/Icon-512.png is excluded by !**/*.png
  • poc/flutter-poc/web/icons/Icon-maskable-192.png is excluded by !**/*.png
  • poc/flutter-poc/web/icons/Icon-maskable-512.png is excluded by !**/*.png
  • poc/ts-vue/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (193)
  • .github/workflows/dart.yml
  • .gitignore
  • Makefile
  • README.md
  • docs/README.md
  • docs/api-reference.md
  • docs/architecture.md
  • docs/configuration.md
  • docs/dart-parity.md
  • docs/getting-started.md
  • docs/github-issue-1-body-issue-only.md
  • docs/github-issue-1-body.md
  • docs/github-issue-dart-full-parity-body.md
  • docs/overview.md
  • docs/platform-adapters.md
  • docs/poc-guide.md
  • docs/poc/simulation.md
  • docs/token-lifecycle.md
  • docs/troubleshooting.md
  • docs/writing-plugins.md
  • package.json
  • packages/dart/morph_core/README.md
  • packages/dart/morph_core/analysis_options.yaml
  • packages/dart/morph_core/lib/morph_core.dart
  • packages/dart/morph_core/lib/src/client/auth_handle.dart
  • packages/dart/morph_core/lib/src/client/host_client.dart
  • packages/dart/morph_core/lib/src/config/ctx_ref.dart
  • packages/dart/morph_core/lib/src/config/exchange_sources.dart
  • packages/dart/morph_core/lib/src/config/interpolate_config.dart
  • packages/dart/morph_core/lib/src/config/list_auth_ids.dart
  • packages/dart/morph_core/lib/src/config/resolved_morph_config.dart
  • packages/dart/morph_core/lib/src/config/validate_config.dart
  • packages/dart/morph_core/lib/src/errors/morph_errors.dart
  • packages/dart/morph_core/lib/src/http/host_pipeline.dart
  • packages/dart/morph_core/lib/src/morph_client.dart
  • packages/dart/morph_core/lib/src/runtime/morph_runtime.dart
  • packages/dart/morph_core/lib/src/runtime/oauth_return_browser.dart
  • packages/dart/morph_core/lib/src/runtime/oauth_return_browser_stub.dart
  • packages/dart/morph_core/lib/src/runtime/plugin_install.dart
  • packages/dart/morph_core/lib/src/types/json_helpers.dart
  • packages/dart/morph_core/lib/src/types/morph_surface.dart
  • packages/dart/morph_core/lib/src/types/morph_types.dart
  • packages/dart/morph_core/lib/src/util/duration_ms.dart
  • packages/dart/morph_core/lib/src/util/expiry.dart
  • packages/dart/morph_core/lib/src/util/http_trace_helpers.dart
  • packages/dart/morph_core/lib/src/util/jwt_utils.dart
  • packages/dart/morph_core/lib/src/util/normalize_origin.dart
  • packages/dart/morph_core/lib/src/util/oauth_authorize.dart
  • packages/dart/morph_core/lib/src/util/oauth_return.dart
  • packages/dart/morph_core/lib/src/util/oauth_state.dart
  • packages/dart/morph_core/lib/src/util/resolve_endpoint.dart
  • packages/dart/morph_core/pubspec.yaml
  • packages/dart/morph_core/test/config_and_morph_client_test.dart
  • packages/dart/morph_core/test/minimal_morph_config.dart
  • packages/dart/morph_core/test/morph_client_bootstrap_test.dart
  • packages/dart/morph_core/test/morph_client_facade_test.dart
  • packages/dart/morph_core/test/oauth_runtime_return_test.dart
  • packages/dart/morph_core/test/plugin_install_test.dart
  • packages/dart/morph_logger/analysis_options.yaml
  • packages/dart/morph_logger/lib/create_logger.dart
  • packages/dart/morph_logger/lib/logger_plugin.dart
  • packages/dart/morph_logger/lib/morph_logger.dart
  • packages/dart/morph_logger/pubspec.yaml
  • packages/dart/morph_logger/test/logger_plugin_test.dart
  • packages/dart/morph_oauth2/analysis_options.yaml
  • packages/dart/morph_oauth2/lib/morph_oauth2.dart
  • packages/dart/morph_oauth2/lib/src/oauth2_plugin.dart
  • packages/dart/morph_oauth2/lib/src/oauth_callbacks.dart
  • packages/dart/morph_oauth2/lib/src/token_http.dart
  • packages/dart/morph_oauth2/lib/src/token_lifecycle.dart
  • packages/dart/morph_oauth2/lib/src/token_vault.dart
  • packages/dart/morph_oauth2/pubspec.yaml
  • packages/dart/morph_oauth2/test/oauth2_plugin_install_test.dart
  • packages/dart/morph_storage/analysis_options.yaml
  • packages/dart/morph_storage/lib/memory_storage_plugin.dart
  • packages/dart/morph_storage/lib/memory_storage_provider.dart
  • packages/dart/morph_storage/lib/morph_storage.dart
  • packages/dart/morph_storage/pubspec.yaml
  • packages/ts/browser-storage/package.json
  • packages/ts/browser-storage/src/browserStorage.ts
  • packages/ts/browser-storage/src/index.ts
  • packages/ts/browser-storage/tsconfig.json
  • packages/ts/browser-storage/vite.config.ts
  • packages/ts/core/package.json
  • packages/ts/core/src/client/AuthHandle.ts
  • packages/ts/core/src/client/HostClient.ts
  • packages/ts/core/src/client/MorphClient.ts
  • packages/ts/core/src/config/interpolate.ts
  • packages/ts/core/src/config/validate.ts
  • packages/ts/core/src/errors.ts
  • packages/ts/core/src/http/hostPipeline.ts
  • packages/ts/core/src/index.ts
  • packages/ts/core/src/runtime.ts
  • packages/ts/core/src/types.ts
  • packages/ts/core/src/util/duration.ts
  • packages/ts/core/src/util/exchangeSources.ts
  • packages/ts/core/src/util/expiry.ts
  • packages/ts/core/src/util/httpTrace.ts
  • packages/ts/core/src/util/jwt.ts
  • packages/ts/core/src/util/normalizeOrigin.ts
  • packages/ts/core/src/util/oauthAuthorize.ts
  • packages/ts/core/src/util/oauthReturn.ts
  • packages/ts/core/src/util/oauthState.ts
  • packages/ts/core/src/util/url.ts
  • packages/ts/core/tsconfig.json
  • packages/ts/core/vite.config.ts
  • packages/ts/logger/package.json
  • packages/ts/logger/src/index.ts
  • packages/ts/logger/tsconfig.json
  • packages/ts/logger/vite.config.ts
  • packages/ts/oauth2/package.json
  • packages/ts/oauth2/src/index.ts
  • packages/ts/oauth2/src/oauth/tokenHttp.ts
  • packages/ts/oauth2/src/tokens/tokenLifecycle.ts
  • packages/ts/oauth2/src/tokens/tokenVault.ts
  • packages/ts/oauth2/src/util/duration.ts
  • packages/ts/oauth2/src/util/exchangeSources.ts
  • packages/ts/oauth2/src/util/expiry.ts
  • packages/ts/oauth2/src/util/interpolate.ts
  • packages/ts/oauth2/src/util/listAuthIds.ts
  • packages/ts/oauth2/src/util/normalizeOrigin.ts
  • packages/ts/oauth2/src/util/oauthAuthorize.ts
  • packages/ts/oauth2/src/util/oauthReturn.ts
  • packages/ts/oauth2/src/util/oauthState.ts
  • packages/ts/oauth2/src/util/url.ts
  • packages/ts/oauth2/tsconfig.json
  • packages/ts/oauth2/vite.config.ts
  • poc/flutter-poc/.gitignore
  • poc/flutter-poc/.metadata
  • poc/flutter-poc/README.md
  • poc/flutter-poc/analysis_options.yaml
  • poc/flutter-poc/android/.gitignore
  • poc/flutter-poc/android/app/build.gradle.kts
  • poc/flutter-poc/android/app/src/debug/AndroidManifest.xml
  • poc/flutter-poc/android/app/src/main/AndroidManifest.xml
  • poc/flutter-poc/android/app/src/main/kotlin/com/burgantech/morph_flutter_poc/MainActivity.kt
  • poc/flutter-poc/android/app/src/main/res/drawable-v21/launch_background.xml
  • poc/flutter-poc/android/app/src/main/res/drawable/launch_background.xml
  • poc/flutter-poc/android/app/src/main/res/values-night/styles.xml
  • poc/flutter-poc/android/app/src/main/res/values/styles.xml
  • poc/flutter-poc/android/app/src/profile/AndroidManifest.xml
  • poc/flutter-poc/android/build.gradle.kts
  • poc/flutter-poc/android/gradle.properties
  • poc/flutter-poc/android/gradle/wrapper/gradle-wrapper.properties
  • poc/flutter-poc/android/settings.gradle.kts
  • poc/flutter-poc/assets/poc-config.json
  • poc/flutter-poc/assets/poc-simulation.json
  • poc/flutter-poc/ios/.gitignore
  • poc/flutter-poc/ios/Flutter/AppFrameworkInfo.plist
  • poc/flutter-poc/ios/Flutter/Debug.xcconfig
  • poc/flutter-poc/ios/Flutter/Release.xcconfig
  • poc/flutter-poc/ios/Podfile
  • poc/flutter-poc/ios/Runner.xcodeproj/project.pbxproj
  • poc/flutter-poc/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  • poc/flutter-poc/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  • poc/flutter-poc/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  • poc/flutter-poc/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  • poc/flutter-poc/ios/Runner.xcworkspace/contents.xcworkspacedata
  • poc/flutter-poc/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  • poc/flutter-poc/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  • poc/flutter-poc/ios/Runner/AppDelegate.swift
  • poc/flutter-poc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
  • poc/flutter-poc/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
  • poc/flutter-poc/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
  • poc/flutter-poc/ios/Runner/Base.lproj/LaunchScreen.storyboard
  • poc/flutter-poc/ios/Runner/Base.lproj/Main.storyboard
  • poc/flutter-poc/ios/Runner/Info.plist
  • poc/flutter-poc/ios/Runner/Runner-Bridging-Header.h
  • poc/flutter-poc/ios/Runner/SceneDelegate.swift
  • poc/flutter-poc/ios/RunnerTests/RunnerTests.swift
  • poc/flutter-poc/lib/main.dart
  • poc/flutter-poc/lib/morph_init.dart
  • poc/flutter-poc/lib/poc_simulation.dart
  • poc/flutter-poc/lib/screens/home_screen.dart
  • poc/flutter-poc/lib/widgets/http_trace_log.dart
  • poc/flutter-poc/lib/widgets/mock_api_sheet.dart
  • poc/flutter-poc/lib/widgets/provider_config_sheet.dart
  • poc/flutter-poc/lib/widgets/simulation_panel.dart
  • poc/flutter-poc/lib/widgets/token_status_card.dart
  • poc/flutter-poc/pubspec.yaml
  • poc/flutter-poc/run_web.sh
  • poc/flutter-poc/test/poc_simulation_test.dart
  • poc/flutter-poc/test/widget_test.dart
  • poc/flutter-poc/web/index.html
  • poc/flutter-poc/web/manifest.json
  • poc/keycloak/morph-realm.json
  • poc/ts-vue/package.json
  • poc/ts-vue/src/hostHttpTraceStore.ts
  • poc/ts-vue/src/morph.ts
  • poc/ts-vue/src/pocSimulationRunner.ts
  • poc/ts-vue/src/pocSimulationWhen.ts
  • poc/ts-vue/src/views/HomeView.vue
  • poc/ts-vue/src/views/OAuthCallbackView.vue

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch f/plugin

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

❤️ Share

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request restructures the repository into a monorepo with TypeScript workspaces under packages/ts/ and introduces a Dart/Flutter SDK port under packages/dart/, complete with a Flutter PoC application. The code review identified several critical issues in the Dart implementation: potential runtime errors in host_pipeline.dart due to unsafe header interpolation, empty body bytes on GET/HEAD requests, and binary body corruption; a performance bottleneck in token_lifecycle.dart from over-locking resolveAccessToken; a deadlock risk in validate_config.dart due to missing cycle detection in exchangeSource configurations; invalid URL formatting in oauth_authorize.dart when query parameters already exist; and a potential type error in token_http.dart when parsing expires_in as a string.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +115 to +148
String? serialized;
if (body == null) {
serialized = null;
} else if (body is String || body is List<int>) {
serialized = body.toString();
} else {
serialized = jsonEncode(body);
}

if (sign) {
if (_options.onSignPayload == null || serialized == null || serialized.isEmpty) {
throw StateError('sign: true requires a JSON body string and onSignPayload');
}
final sig = await _options.onSignPayload!(serialized, authId);
headers = {...?headers, 'X-JWS-Signature': sig};
}

final mergedHost = interpolateRecord(host.headers, _variables);
final headerMap = <String, String>{
...?mergedHost,
...?_interpolateHeaders(headers),
headerCfg.name: '${headerCfg.scheme} $token',
};

Future<http.Response> once(Map<String, String> hdrs) async {
final req = http.Request(method, uri);
req.headers.addAll(hdrs);
final b = serialized;
req.bodyBytes = utf8.encode(b ?? '');
if (serialized != null) {
req.headers['content-type'] = req.headers['content-type'] ?? 'application/json;charset=utf-8';
}
return _client.send(req).timeout(Duration(milliseconds: timeoutMs)).then(http.Response.fromStream);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

There are three distinct issues in this block:\n\n1. Request-time headers interpolation: Passing request-time headers to _interpolateHeaders is unsafe. Programmatically generated headers (like cryptographic signatures or custom tokens) can contain $ characters, which will trigger interpolateString and throw a StateError at runtime.\n2. Empty bodyBytes for GET/HEAD requests: Setting req.bodyBytes to utf8.encode(b ?? '') when b is null forces an empty body ([]) and a Content-Length: 0 header, which can cause issues with some servers/proxies for GET or HEAD requests.\n3. Binary body corruption: Calling body.toString() on a List<int> converts it to a literal string representation (e.g., "[1, 2, 3]") instead of sending the raw binary bytes.

Suggested change
String? serialized;
if (body == null) {
serialized = null;
} else if (body is String || body is List<int>) {
serialized = body.toString();
} else {
serialized = jsonEncode(body);
}
if (sign) {
if (_options.onSignPayload == null || serialized == null || serialized.isEmpty) {
throw StateError('sign: true requires a JSON body string and onSignPayload');
}
final sig = await _options.onSignPayload!(serialized, authId);
headers = {...?headers, 'X-JWS-Signature': sig};
}
final mergedHost = interpolateRecord(host.headers, _variables);
final headerMap = <String, String>{
...?mergedHost,
...?_interpolateHeaders(headers),
headerCfg.name: '${headerCfg.scheme} $token',
};
Future<http.Response> once(Map<String, String> hdrs) async {
final req = http.Request(method, uri);
req.headers.addAll(hdrs);
final b = serialized;
req.bodyBytes = utf8.encode(b ?? '');
if (serialized != null) {
req.headers['content-type'] = req.headers['content-type'] ?? 'application/json;charset=utf-8';
}
return _client.send(req).timeout(Duration(milliseconds: timeoutMs)).then(http.Response.fromStream);
}
String? serialized;\n List<int>? bodyBytes;\n if (body == null) {\n serialized = null;\n } else if (body is List<int>) {\n bodyBytes = body;\n } else if (body is String) {\n serialized = body;\n } else {\n serialized = jsonEncode(body);\n }\n\n if (sign) {\n if (_options.onSignPayload == null || serialized == null || serialized.isEmpty) {\n throw StateError('sign: true requires a JSON body string and onSignPayload');\n }\n final sig = await _options.onSignPayload!(serialized, authId);\n headers = {...?headers, 'X-JWS-Signature': sig};\n }\n\n final mergedHost = interpolateRecord(host.headers, _variables);\n final headerMap = <String, String>{\n ...?mergedHost,\n ...?headers,\n headerCfg.name: '${headerCfg.scheme} $token',\n };\n\n Future<http.Response> once(Map<String, String> hdrs) async {\n final req = http.Request(method, uri);\n req.headers.addAll(hdrs);\n if (bodyBytes != null) {\n req.bodyBytes = bodyBytes;\n } else if (serialized != null) {\n req.bodyBytes = utf8.encode(serialized);\n req.headers['content-type'] = req.headers['content-type'] ?? 'application/json;charset=utf-8';\n }\n return _client.send(req).timeout(Duration(milliseconds: timeoutMs)).then(http.Response.fromStream);\n }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in commit 0bbf751. Three bugs addressed: (1) List body now uses req.bodyBytes directly — no more toString() corruption. (2) GET/HEAD with no body no longer sets bodyBytes = utf8.encode('') — no spurious Content-Length: 0. (3) Request-time headers are no longer passed through _interpolateHeaders — host config headers are interpolated from variables, request headers passed verbatim.

Comment on lines +339 to +341
Future<String> resolveAccessToken(String aid, CtxRef ref, String mode) =>
_withLock<String>(aid, () => _resolveAccessInner(aid, ref, mode));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Wrapping the entire resolveAccessToken process in _withLock serializes all concurrent requests even when the token is already valid and just needs to be read from storage. This introduces a major performance bottleneck, especially on mobile platforms where secure storage reads are slow.\n\nConsider:\n1. Implementing an in-memory cache of the active TokenSet in TokenLifecycle or TokenVault so that valid tokens can be returned synchronously/immediately without async storage reads.\n2. Only acquiring the lock when a token refresh or exchange is actually required, rather than locking the entire resolution process.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in commit 0bbf751. Added _tokenCache (Map<String, TokenSet>) to TokenLifecycle, populated on every _persist call and invalidated on clear/logout. resolveAccessToken now checks the cache first and returns immediately if the token is still valid, skipping both the lock and the storage read on the hot path.

Comment on lines +91 to +99
for (final entry in contextByAuthId.entries) {
final authId = entry.key;
final c = entry.value.context;
for (final src in normalizeExchangeSourcesFromTokenBlock(c.token)) {
if (!contextByAuthId.containsKey(src)) {
errors.add('$authId: token.exchangeSource references unknown context $src');
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

There is no cycle detection for exchangeSource configurations. Circular dependencies (e.g., A -> B -> A) will cause a silent deadlock at runtime due to nested _withLock calls in TokenLifecycle.resolveAccessToken.\n\nConsider adding a cycle detection check using a simple DFS/visited set during config validation to catch these errors early.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in commit 0bbf751. Added DFS cycle detection in validateAndIndexConfig after the unknown-ref check. Circular exchangeSource chains (A->B->A) now raise a ConfigValidationError at init time instead of silently deadlocking at runtime.

if (scopes != null && scopes.isNotEmpty) {
q['scope'] = scopes.join(' ');
}
final buf = StringBuffer('$u?');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Blindly appending ? to the resolved endpoint will result in invalid URL syntax if the endpoint already contains query parameters (e.g., /oauth/authorize?tenant=123).

Suggested change
final buf = StringBuffer('$u?');
final buf = StringBuffer(u)..write(u.contains('?') ? '&' : '?');

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in commit 0bbf751. Now uses u.contains('?') ? '&' : '?' before appending query params, so endpoints that already contain query parameters are handled correctly.

Comment on lines +20 to +28
OAuthTokenResponse oauthResponseFromJson(dynamic json) {
if (json is! Map) throw FormatException('OAuth token JSON must be object');
final m = Map<String, dynamic>.from(json.cast<dynamic, dynamic>());
return OAuthTokenResponse(
accessToken: m['access_token'] as String? ?? '',
refreshToken: m['refresh_token'] as String?,
expiresIn: (m['expires_in'] as num?)?.toInt(),
tokenType: m['token_type'] as String?,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Casting expires_in directly to num? and calling toInt() will throw a TypeError at runtime if an identity provider returns expires_in as a string (e.g., "3600"). Parsing should be robust enough to handle both types.

OAuthTokenResponse oauthResponseFromJson(dynamic json) {\n  if (json is! Map) throw FormatException('OAuth token JSON must be object');\n  final m = Map<String, dynamic>.from(json.cast<dynamic, dynamic>());\n  final rawExpiresIn = m['expires_in'];\n  int? expiresIn;\n  if (rawExpiresIn is num) {\n    expiresIn = rawExpiresIn.toInt();\n  } else if (rawExpiresIn is String) {\n    expiresIn = int.tryParse(rawExpiresIn);\n  }\n  return OAuthTokenResponse(\n    accessToken: m['access_token'] as String? ?? '',\n    refreshToken: m['refresh_token'] as String?,\n    expiresIn: expiresIn,\n    tokenType: m['token_type'] as String?,\n  );\n}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in commit 0bbf751. expires_in is now parsed robustly: num -> toInt(), String -> int.tryParse(), anything else -> null. No more TypeError when an identity provider returns a string.

**High #1 — host_pipeline.dart: three body/header bugs**
- List<int> body was calling toString() → binary corruption; now uses bodyBytes directly
- GET/HEAD with no body no longer sets bodyBytes = utf8.encode('') → no spurious Content-Length: 0
- Request-time headers (may contain $ in JWS signatures) are no longer passed through
  interpolateString; only host config headers are interpolated from variables

**High #2 — token_lifecycle.dart: resolveAccessToken lock contention**
- Add _tokenCache (Map<String, TokenSet>) populated on every _persist
- resolveAccessToken checks cache first; returns immediately if token is valid
  (skips lock acquisition and storage read on the hot path)
- Cache is invalidated on clear/logout via _persist(set: null)

**Medium #1 — validate_config.dart: no exchangeSource cycle detection**
- Add DFS cycle detection after the unknown-ref check
- Circular A→B→A chains now raise a ConfigValidationError at init time
  instead of deadlocking at runtime via nested _withLock calls

**Medium #2 — oauth_authorize.dart: blindly appending '?'**
- Use '&' instead of '?' when the resolved endpoint already contains query params

**Medium #3 — token_http.dart: expires_in string cast**
- Parse expires_in robustly: num.toInt() or int.tryParse(String)
  so identity providers returning "3600" no longer throw TypeError
- validate_config: rename _dfsSeen/_hasCycle local vars (no leading underscore for locals)
- host_pipeline: remove now-unused _interpolateHeaders method (headers no longer interpolated at request time per Gemini fix)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants