Skip to content

Ad4m connect updates#646

Merged
jhweir merged 40 commits intodevfrom
ad4m-connect-updates
Jan 24, 2026
Merged

Ad4m connect updates#646
jhweir merged 40 commits intodevfrom
ad4m-connect-updates

Conversation

@jhweir
Copy link
Contributor

@jhweir jhweir commented Jan 21, 2026

AD4M Connect: Architecture Refactor & Code Consolidation

Overview

This PR represents a comprehensive architectural refactor of @coasys/ad4m-connect, consolidating 19 components into 4 focused view components + 11 SVG icons, and reducing the overall codebase by 50%. The refactor builds upon the multi-user authentication features added in the previous multi-user branch, providing a cleaner, more maintainable foundation with clear separation of concerns and a simplified developer experience.

Note: Multi-user authentication features (email-based auth, passwordless verification, account creation) were implemented in the previous multi-user branch. This refactor focuses on consolidating that implementation into a better architecture.

🎯 Key Improvements

Architecture Consolidation

  • Component reduction: 19 components → 4 components (79% reduction)
    • 3 focused view components (ConnectionOptions, LocalAuthentication, RemoteAuthentication)
    • 1 main orchestrator (Ad4mConnectUI in web.ts)
    • 11 SVG icon components (pure markup, no logic)
  • Code reduction: ~2000+ lines → ~1000 lines (50% reduction)
  • Clear separation: Business logic (core.ts) completely separate from UI (web.ts and view components)

Single Unified API

  • Before: Multiple initialization patterns, inconsistent behavior
  • After: Single await getAd4mClient(options) that works everywhere
  • Automatic mode detection: Embedded vs standalone handled internally
  • Consistent behavior: Same API surface regardless of environment

Event-Driven Architecture

  • Core extends EventTarget, dispatches: authstatechange, connectionstatechange, configstatechange
  • View components dispatch action events: verify-code, email-login, connect-remote-node, etc.
  • Parent-child communication via CustomEvents with bubbles: true, composed: true
  • Clean separation: UI never directly accesses client, always goes through core

Embedded Mode Support

  • Automatic detection via window.self !== window.top
  • postMessage protocol for parent-child iframe communication
  • Config injection from host applications (WE launcher)
  • UI suppression in embedded mode (no modal shown)
  • Automatic initialization when AD4M_CONFIG received

📊 Impact Summary

Code Quality

  • 50% code reduction: ~2000+ lines → ~1000 lines
  • 79% component reduction: 19 → 4 focused components (+ 11 SVG icons)
  • TypeScript strict mode: Full type safety throughout
  • Event-driven: Clean component communication
  • No circular dependencies: Clean dependency tree

Architecture

  • Separation of concerns:
    • core.ts - All business logic, connection management, authentication
    • web.ts - UI orchestration, view routing, state coordination
    • View components - Single-responsibility, focused UI
    • shared-styles.ts - Consistent styling system
  • Reusable patterns:
    • withTempClient() - Temporary client creation with automatic cleanup
    • createApolloClient() - Centralized Apollo configuration
    • buildAppInfo() - App metadata construction
    • Auto-bind pattern for consistent this context
  • Maintainability: Smaller, focused components with clear responsibilities

Developer Experience

  • ✅ Single API entry point
  • ✅ Automatic mode detection (embedded/standalone)
  • ✅ Auto-reconnect with stored credentials
  • ✅ No manual postMessage handling
  • ✅ Token expiry handling
  • ✅ Clear error states

🔄 Breaking Changes

Main API Change

Before:

import { getAd4mClient } from "@coasys/ad4m-connect";

// Synchronous, might return undefined
const client = getAd4mClient();

// Or with hooks
const { client } = useClient();

After - Two Approaches:

Simple API (Recommended)

Returns just the authenticated client:

import { getAd4mClient } from "@coasys/ad4m-connect";

const client = await getAd4mClient({
  appInfo: {
    name: "My App",
    description: "Description",
    url: window.location.origin,
    iconPath: "/icon.png",
  },
  capabilities: [
    /* ... */
  ],
});

// Client is ready to use immediately
const agentStatus = await client.agent.status();

Advanced API

Returns core and client separately, allowing event listener attachment before authentication:

import { getAd4mConnect } from "@coasys/ad4m-connect";

const { core, client } = getAd4mConnect({
  appInfo: {
    /* ... */
  },
  capabilities: [
    /* ... */
  ],
});

// Attach listeners before authentication completes
core.addEventListener("connectionstatechange", (e) => {
  console.log("Connection state:", e.detail);
});

core.addEventListener("authstatechange", (e) => {
  console.log("Auth state:", e.detail);
});

// Await client when needed
const ad4mClient = await client;

When to use each:

  • Simple API: Most use cases - just need the client after authentication
  • Advanced API: Need to track connection/auth state changes, or access core methods directly

Package Exports

  • Main export changed from ./web.js to ./index.js
  • Removed useClient hooks from React and Vue packages (redundant with explicit client passing)

Configuration Options

  • backendUrl renamed to remoteUrl for clarity
  • Removed deprecated hosting, mobile, multiUser options (auto-detected)

🏗️ Architecture

Before: Fragmented Architecture

// Scattered across many files
@customElement("ad4m-connect")
class Ad4mConnect extends LitElement {
  // Authentication logic mixed with UI
  // Connection management mixed with rendering
  // State scattered across multiple @state() decorators
}

After: Separation of Concerns

// core.ts - Pure business logic
class Ad4mConnect extends EventTarget {
  async connect(): Promise<Ad4mClient>;
  async requestCapability(): Promise<string>;
  async verifyLocalAd4mCode(code: string): Promise<boolean>;
  async submitEmail(email: string): Promise<VerificationRequestResult>;
  async verifyEmailCode(email: string, code: string): Promise<boolean>;
  async loginWithPassword(email: string, password: string): Promise<boolean>;

  // Dispatches events: authstatechange, connectionstatechange
}

// web.ts - UI orchestration
@customElement("ad4m-connect")
class Ad4mConnectElement extends LitElement {
  core: Ad4mConnect;
  @state() currentView: Views;
  @state() modalOpen = false;

  // Routes to appropriate view component
  renderViews() {
    /* ... */
  }
}

// View components - Single responsibility
@customElement("connection-options")
class ConnectionOptions extends LitElement {
  // Just UI, dispatches events
  private connectLocalNode() {
    this.dispatchEvent(
      new CustomEvent("connect-local-node", {
        bubbles: true,
        composed: true,
      }),
    );
  }
}

📝 Detailed Changes

Core Logic (src/core.ts)

Key responsibilities:

  • Connection lifecycle management (connect, buildClient, checkAuth)
  • Embedded mode initialization (postMessage protocol)
  • Local authentication (capability request, code verification)
  • Remote authentication (email, password, account creation)
  • Event dispatching for state changes

Embedded Mode:

constructor(options: Ad4mConnectOptions) {
  super();
  this.embedded = isEmbedded();
  if (this.embedded) this.initializeEmbeddedMode();
}

private initializeEmbeddedMode(): void {
  window.addEventListener('message', async (event: MessageEvent) => {
    if (event.data?.type === 'AD4M_CONFIG') {
      const { port, token } = event.data;
      this.port = port;
      this.token = token;
      this.url = `ws://localhost:${port}/graphql`;

      // Store and connect
      setLocal('ad4m-port', port.toString());
      setLocal('ad4m-token', token);
      setLocal('ad4m-url', this.url);

      this.ad4mClient = await this.buildClient();
      await this.checkAuth();
    }
  });

  window.parent.postMessage({ type: 'REQUEST_AD4M_CONFIG' }, '*');
}

Helper Patterns:

// Temporary client for API validation (auto-cleanup)
private async withTempClient<T>(
  url: string,
  callback: (client: Ad4mClient) => Promise<T>
): Promise<T> {
  const wsClient = createClient({ url, connectionParams: /*...*/ });
  const apolloClient = this.createApolloClient(wsClient);
  const client = new Ad4mClient(apolloClient);

  try {
    return await callback(client);
  } finally {
    wsClient.dispose();
  }
}

// Centralized Apollo configuration
private createApolloClient(wsClient: WSClient) {
  return new ApolloClient({
    link: new GraphQLWsLink(wsClient),
    cache: new InMemoryCache({ resultCaching: false, addTypename: false }),
    defaultOptions: {
      watchQuery: { fetchPolicy: "no-cache" },
      query: { fetchPolicy: "no-cache" },
      mutate: { fetchPolicy: "no-cache" }
    }
  });
}

Web Orchestrator (src/web.ts)

Key responsibilities:

  • Modal visibility management
  • View routing (connection-options, local-authentication, remote-authentication)
  • Event handling (delegates to core)
  • UI state coordination (loading, errors)

View Routing:

@state() currentView: Views = "connection-options";

renderViews() {
  if (this.currentView === "connection-options") {
    return html`<connection-options
      .port=${this.core.port}
      .remoteUrl=${this.core.options.remoteUrl}
      .connectingToRemoteNode=${this.connectingToRemoteNode}
      .remoteNodeError=${this.remoteNodeError}
      @connect-local-node=${this.connectLocalNode}
      @connect-remote-node=${this.connectRemoteNode}
    ></connection-options>`;
  }

  if (this.currentView === "local-authentication") {
    return html`<local-authentication
      .capabilities=${this.core.options.capabilities}
      .appname=${this.core.options.appInfo.name}
      @verify-code=${this.verifyLocalAd4mCode}
    ></local-authentication>`;
  }

  // ... remote-authentication
}

Event Delegation:

private async connectLocalNode() {
  // Update core state
  this.core.url = `ws://localhost:${this.core.port}/graphql`;
  setLocal("ad4m-url", this.core.url);

  // Delegate to core
  await this.core.connect();

  // Update UI
  this.currentView = "local-authentication";
}

private async verifyLocalAd4mCode(event: CustomEvent) {
  const success = await this.core.verifyLocalAd4mCode(event.detail.code);
  this.verificationError = !success;
  if (success) this.modalOpen = false;
}

View Components

ConnectionOptions (src/components/views/ConnectionOptions.ts)

  • Detects local node on specified port
  • Allows remote URL input
  • Dispatches: connect-local-node, connect-remote-node, change-port

LocalAuthentication (src/components/views/LocalAuthentication.ts)

  • Shows app capabilities
  • 6-digit code input with auto-submit
  • Dispatches: request-capability, verify-code

RemoteAuthentication (src/components/views/RemoteAuthentication.ts)

  • Multi-step email authentication flow
  • Conditional rendering based on remoteAuthState
  • Dispatches: email-login, verify-email-code, password-login, create-account

Shared Styles (src/styles/shared-styles.ts)

Consistent design system:

export const sharedStyles = css`
  /* CSS Variables */
  :host {
    --ac-primary-color: #91e3fd;
    --ac-success-color: #5dd27d;
    --ac-danger-color: #f4367f;
    --ac-border-color-light: #91d4fd69;
  }

  /* Typography */
  h1 {
    font-size: 28px;
    font-weight: 700;
  }
  h3 {
    font-size: 18px;
    font-weight: 400;
  }

  /* Buttons */
  button.primary {
    background: var(--ac-primary-color);
    color: #000000;
    height: 48px;
    padding: 0 24px;
    border-radius: 8px;
  }

  /* Inputs */
  input {
    height: 48px;
    padding: 0 16px;
    border-radius: 8px;
    background: #00091e5c;
    color: #ffffff;
  }

  /* State indicators */
  .state.success {
    color: var(--ac-success-color);
  }
  .state.danger {
    color: var(--ac-danger-color);
  }
`;

🧪 Testing Scenarios

Standalone Mode

  1. First-time user (local)

    • Opens app → Modal appears
    • Detects local node on port 12000
    • Clicks "Connect to Local Node"
    • Enters 6-digit code → Auto-submits
    • Connected ✓
  2. First-time user (remote)

    • Opens app → Modal appears
    • Enters remote URL
    • Enters email → Server responds with flow
    • Completes auth (code or password)
    • Connected ✓
  3. Returning user

    • Opens app → No modal (auto-reconnect with stored token)
    • If token valid → Connected immediately ✓
    • If token expired → Modal appears

Embedded Mode

  1. WE Launcher integration

    • App loaded in iframe
    • Sends REQUEST_AD4M_CONFIG
    • Receives AD4M_CONFIG with port/token
    • Builds client silently, no UI ✓
  2. Host app integration

    • Parent injects config via postMessage
    • Child receives and initializes
    • No localStorage conflicts ✓

Error Scenarios

  1. Invalid token → Auto-reconnect fails → Modal shows
  2. Network failure → Clear error message, retry available
  3. Expired code → User informed, can request new

🔄 Migration Guide

Update Initialization

// Before
import { getAd4mConnect } from "@coasys/ad4m-connect";
const { core, client } = await getAd4mConnect({
  appInfo: {
    /*...*/
  },
});

// After
import { getAd4mClient } from "@coasys/ad4m-connect";
const client = await getAd4mClient({
  appInfo: {
    /*...*/
  },
});

Remove useClient Hooks

// Before - React
import { useClient } from '@coasys/ad4m-react-hooks';
function MyComponent() {
  const { client } = useClient();
}

// After - Pass client from parent
function App() {
  const [client, setClient] = useState(null);

  useEffect(() => {
    getAd4mClient(options).then(setClient);
  }, []);

  return client ? <MyComponent client={client} /> : <Loading />;
}

Remove Embedded Mode Logic

// Before - Manual iframe detection
if (window.self !== window.top) {
  window.addEventListener("message", handleAd4mConfig);
} else {
  const { core } = await getAd4mConnect(options);
}

// After - Automatic
const client = await getAd4mClient(options);
// Works in both embedded and standalone automatically

📦 Files Changed

Core Package

  • src/index.ts - Entry point exposing getAd4mClient() and getAd4mConnect() APIs
  • src/core.ts - Business logic extending EventTarget
  • src/web.ts - UI orchestrator component
  • src/types.ts - TypeScript type definitions
  • src/utils.ts - Utilities (isEmbedded, localStorage, connectWebSocket)
  • src/styles/shared-styles.ts - Shared CSS

View Components

  • src/components/views/ConnectionOptions.ts
  • src/components/views/LocalAuthentication.ts
  • src/components/views/RemoteAuthentication.ts

Icon Components (11 SVG components)

  • Ad4mLogo, ArrowLeftIcon, ArrowLeftRightIcon, CheckIcon, CrossIcon, DownloadIcon, LocalIcon, QRCodeIcon, RefreshIcon, RemoteIcon

Configuration

  • package.json - Updated main export to index.js
  • tsconfig.json - Added index.ts to includes

🎯 Future Enhancements

  1. QR Code Scanning - For mobile device pairing
  2. Session Management - Multi-device session tracking
  3. Remember Me - Persistent authentication toggle
  4. Biometric Auth - WebAuthn integration
  5. Dark Mode - Theme support
  6. Internationalization - Multi-language support

✅ Checklist

  • TypeScript strict mode enabled
  • No console warnings
  • Breaking changes documented
  • Migration guide provided
  • Event-driven architecture implemented
  • Error handling comprehensive
  • Loading states consistent
  • Embedded mode tested with WE launcher
  • Auto-reconnect working
  • Token expiry handled

🤝 Related PRs

  • Flux PR: AD4M Connect Refactor & Initialization Improvements (companion PR)

Review Focus:

  1. Architecture - Separation of concerns between core.ts and web.ts
  2. Event flow - CustomEvents bubble correctly through shadow DOM
  3. Embedded mode - postMessage protocol works with WE launcher
  4. Error handling - All paths have proper error states
  5. Resource cleanup - Temp clients disposed properly

Testing Priority:

  1. ✓ Standalone mode with local executor
  2. ✓ Standalone mode with remote node
  3. ✓ Embedded mode in WE launcher
  4. ✓ Page refresh with stored token
  5. ✓ Token expiry handling

Summary by CodeRabbit

  • New Features

    • Embedded mode with faster auto-connect and a streamlined two-tier connect API.
    • Redesigned connection UI: Connection Options plus Local and Remote authentication flows.
    • New icon set, shared styling, and reusable UI view components for a modern look.
  • Refactor

    • Core-driven, event-based connect flow replacing legacy UI/state handling.
  • Chores

    • Updated package manifests, publishing metadata, dependency specs, and bumped versions.

✏️ Tip: You can customize this high-level summary in your review settings.

jhweir added 21 commits January 7, 2026 19:02
…fresh

- Store selected backend URL in localStorage when connection is established
- Check stored URL before configured backendUrl in auto-reconnect logic
- Ensures local launcher connections persist across page refreshes
- Prevents forced reconnection to remote backend when user chose local

Modified:
- connect/src/core.ts: Added setForVersion('ad4murl', url) in setUrl()
- connect/src/web.ts: Check getForVersion("ad4murl") before this.backendUrl in connectedCallback()
Add automatic detection and handling of embedded mode (iframe) in ad4m-connect to eliminate need for apps to implement their own postMessage logic.

Changes:
- core.ts: Detect embedded mode via window.self !== window.top check
- core.ts: Add initializeEmbeddedMode() to handle postMessage protocol
- core.ts: Listen for AD4M_CONFIG from parent and build client with received credentials
- core.ts: Ignore localStorage tokens in embedded mode to prevent stale token issues
- core.ts: Update connect() method to wait for postMessage in embedded mode
- web.ts: Prevent UI from rendering when running in embedded mode

Benefits:
- Apps no longer need embedded vs standalone branching logic
- Single unified getAd4mClient() API works in both modes
- No modal shown in embedded mode
- Prevents InvalidToken errors from stale localStorage in dev hot reloads
- Follows established postMessage request-response pattern from WE launcher

Modified files:
- connect/src/core.ts
- connect/src/web.ts
…emove redundant hooks

BREAKING CHANGES:
- Main export changed from './web.js' to './index.js' in package.json
- getAd4mClient() now returns Promise<Ad4mClient> instead of sync client retrieval
- Removed DOM-based getAd4mClient() from utils.ts (now re-exports from index.ts)

ad4m-connect:
- Created new index.ts entry point with simplified getAd4mClient(options) function
- Handles both embedded (iframe) and standalone modes automatically
- Returns Promise that resolves when authenticated
- Fixed connect() to return Promise resolving on authentication in standalone mode
- Added EventTarget inheritance to Ad4mConnect for standard DOM events
- Persists localStorage fallback for port/url/token in standalone mode
- Re-exports getAd4mClient from utils.ts for backwards compatibility

ad4m-hooks:
- Removed useClient hooks from React and Vue packages (redundant with centralized init)
- Updated exports to remove useClient from both packages
- Hooks-helpers getProfile reverted to old signature (will be updated when packages are published)

Build:
- Added build:index script to build new index.ts entry point
- Updated main export path in package.json

Note: The getProfile function in hooks-helpers still uses the old API for now to avoid
breaking builds. This should be updated to require client parameter when publishing new versions.
- Change getAd4mConnect to return core synchronously and client as Promise
- Enables event listener attachment before authentication completes
- Update getAd4mClient to use non-async destructuring
- Add TypeScript type declarations to package.json exports
- Simplify JSDoc comments with clear Simple/Advanced API distinction
- Add @example tags showing usage patterns for both APIs

BREAKING CHANGE: getAd4mConnect now returns { core, client } synchronously
instead of Promise<{ core, client }>. Apps using getAd4mConnect need to
await the client property: const { core, client } = getAd4mConnect(options);
const ad4mClient = await client;
- Add isEmbedded() utility function in utils.ts
- Remove duplicate isEmbedded() method from Ad4mConnect class
- Update core.ts to import and use isEmbedded() utility
- Update web.ts to use standalone isEmbedded() function
- Export isEmbedded from index.ts for external use

Single source of truth for embedded mode detection across codebase.
- Import and use isEmbedded() utility instead of inline check
- In standalone mode, return Promise that resolves on auth completion
- Don't auto-call core.connect() - let UI handle local vs remote choice
- Remove auth timeout (apps can implement their own if needed)

This fixes the issue where standalone apps would auto-connect to local
AD4M without showing the connection overview UI where users can choose
between local executor or remote multi-user mode.
These props need to be set on the element for proper rendering of
multi-user authentication flows and remote connection configuration.
Set display:none on element when stored token exists, only showing UI
if authentication fails. Prevents brief flash of connection UI on page
refresh when user is already authenticated.
- Created ConnectState class using Lit's ReactiveController pattern
- Migrated 20+ @State() decorators to centralized state management
- Added type-safe state interfaces (FormState, ErrorState, LoadingState, DetectionState)
- Implemented computed properties (canSubmitMultiUser, showLocalOption, etc.)
- Added type-safe update methods (updateForm, setError, setLoading, etc.)
- Updated all component methods to use new state API
- Improved code maintainability and state flow clarity
- Removed duplicate changeCode method

Benefits:
- Single source of truth for all UI state
- Better type safety with generic update methods
- Clearer state flow with explicit update calls
- Computed properties eliminate duplicate logic
- Easier to test and reason about state changes
Centralize duplicated code patterns in both core and web components to improve
maintainability and reduce code duplication by ~140 lines.

Core (src/core.ts):
- Add createApolloClient() helper to centralize Apollo configuration
  - Eliminates duplicate InMemoryCache and defaultOptions setup
  - Used in both buildClient() and buildTempClient()
- Add notifyDisconnected() helper for common error/disconnection pattern
  - Replaces 6 instances of paired notifyConnectionChange + notifyAuthChange calls
  - Supports optional connection state parameter
- Remove incorrect deprecation warning from connectMultiUser()
  - Method is still valid for programmatic multi-user authentication
  - Clarify documentation for UI-driven vs programmatic flows

Web (src/web.ts):
- Add initializeClient() to consolidate client initialization logic
  - Eliminates duplicate client creation in initializeWithoutUI() and connectedCallback()
- Add setupEventListeners() to centralize event listener registration
  - Ensures consistent event handling across initialization paths
- Add buildAppInfo() to create authentication info object
  - Replaces 3 duplicate object constructions
- Add completeAuthentication() for post-auth flow
  - Consolidates token storage, client rebuild, and UI state updates
  - Replaces 3 nearly-identical blocks (~12 lines each)
- Add clearCodeSubmitTimeout() for verification code timeout management
  - Replaces 3 duplicate timeout clearing patterns

Benefits:
- Reduced code duplication by ~140 lines total
- Single source of truth for Apollo configuration
- Consistent error handling and state notification
- Easier to maintain and test isolated helper methods
- Clear separation of concerns with well-named methods

Breaking changes: None (all changes are internal refactoring)
- Reorganize component structure into views/, states/, and shared/ folders
- Create unified ErrorState component replacing 4 separate error components
- Convert 8 view components from functional templates to @CustomElement web components:
  * ConnectionOverview, RemoteConnection, ScanQRCode, RequestCapability
  * Settings, Start, Hosting, MultiUserAuth
- Replace custom HTML/CSS with Flux UI components (j-flex, j-text, j-button, j-input, j-box, j-spinner)
- Update web.ts to instantiate Lit components with CustomEvent-based communication
- Remove 6 redundant components (AgentLocked, Disconnected, CouldNotMakeRequest, InvalidToken, Loading, MobileAppLogoButton)
- Component count reduced from 19 to 12 (37% reduction)

This migration improves code maintainability, reduces duplication, and provides
consistent styling through the Flux UI design system while preserving all
existing functionality.
- Reverted all 8 view components from Flux UI to native HTML elements
  - Replaced j-flex → div with flexbox
  - Replaced j-text → h1/h2/h3/p elements
  - Replaced j-button → button with CSS classes
  - Replaced j-box → div
  - Replaced j-input → input
  - Replaced j-spinner → custom CSS spinner animation

- Created shared-styles.ts for centralized styling
  - Typography (h1, h2, h3, p, label)
  - Button variants (primary, secondary, ghost, link)
  - Form inputs with focus/disabled states
  - Common layout utilities (container, button-row, center)
  - Error and info boxes

- Updated components to import and use shared styles
  - ConnectionOverview, RemoteConnection, ScanQRCode
  - RequestCapability, Settings, Start
  - Hosting, MultiUserAuth

- Removed @coasys/flux-ui peer dependency from package.json

Rationale: Shadow DOM boundaries prevented CSS variable inheritance
for component-level theming. Native HTML approach provides full control
over styling and is better suited for reusable package architecture.
- Rename mobileView() to settingsButton() for clarity - describes both purpose and behavior
- Remove CSS variable definitions from shared-styles.ts to fix shadow DOM inheritance issues
- Update shared-styles.ts to consume CSS vars from web.ts instead of defining its own
- Add standardized CSS variables to web.ts :host block:
  - --ac-background-color for modal background
  - --ac-border-color-dark and --ac-border-color-light for consistent borders
- Update modal styling to use new CSS variable system
- Remove outdated TODO comments about localStorage (functionality works correctly)
- Rename AppLogo import to Ad4mLogo for consistency

This fixes the issue where each component was defining its own CSS variables due to
:host in shared-styles, preventing proper theming from the parent component.
…ture

- Change Ad4mConnectUI factory to inject core instance instead of creating internally
- Make core property public to allow factory injection
- Implement new ConnectionOptions and LocalAuthentication view components
- Add reactive property updates with requestUpdate() for non-reactive core mutations
- Set up event-driven communication between parent and child components using CustomEvents
- Remove redundant checkAuth() call from buildClient connected callback
- Add willUpdate() lifecycle for syncing parent props to child state
- Implement port/URL change handlers with proper event bubbling (bubbles: true, composed: true)
- Add 6-digit numeric security code input with monospace styling
- Separate input state (newPort) from committed state (port) for better UX

Breaking changes:
- Ad4mConnectUI now requires Ad4mConnect instance as parameter
- Core property is now public instead of private
- Add back button to LocalAuthentication view with ArrowLeftIcon component
- Implement auto-submit when 6-digit security code is entered
- Add verification error handling and display
- Refactor input validation to block non-numeric characters immediately
- Fix infinite reconnection loop by implementing requestedRestart flag
- Add state indicators for success and error states
- Consolidate CSS state styles in shared-styles
- Clean up commented code and improve code organization
- Update verifyCode to use connect() instead of buildClient() directly
…tence

Major refactoring of AD4M Connect authentication system with improved architecture and UX:

**Core Features Added:**
- Complete multi-user authentication flow with smart detection
  - Email submission with server-side routing
  - Passwordless verification via email code (for existing users)
  - Password-based login (for returning users requiring password)
  - Account creation flow (for new users)
- Embedded mode support with postMessage protocol
  - Parent-child iframe communication
  - AD4M config injection from host application
- localStorage persistence for connection state
  - Port, URL, and token storage
  - Auto-reconnect with stored credentials
  - UI visibility management based on auth state

**Components:**
- New RemoteAuthentication component for multi-user flows
- Enhanced ConnectionOptions with remote node validation
- Improved LocalAuthentication with auto-submit on 6-digit code entry
- Consolidated shared styles with back button and state indicators

**Architecture Improvements:**
- Business logic separated into core.ts
- View orchestration handled by web.ts
- Event-driven communication between components
- MutationObserver pattern for reactive UI visibility
- Proper error handling with try-catch-finally blocks
- requestedRestart flag to prevent reconnection loops

**Bug Fixes:**
- Fixed infinite reconnection loop after authentication
- Input validation for security codes (numeric only)
- Auth listener setup before connection attempt
- Token expiry handling with automatic UI display
- API validation with proper error propagation
- Clear error messages for each authentication step

**Code Quality:**
- Reduced codebase from 2000+ to ~1000 lines
- TypeScript strict mode compliance
- Consistent error handling patterns
- Improved state management across components
…tency

Refactored AD4M Connect codebase for better maintainability and user experience:

**Code Organization (core.ts):**
- Reordered methods by logical grouping (connection flow, embedded mode, local auth, remote auth)
- Moved private helper methods (notifyX) to bottom of class
- Grouped related methods together (buildClient with createApolloClient)
- Added section comments for better code navigation
- Simplified constructor logic (unified embedded/standalone initialization)

**Loading State Improvements (web.ts):**
- Added remoteAuthLoading state to verifyEmailCode, passwordLogin, and createAccount
- Ensured consistent loading feedback across all remote authentication methods
- Added finally blocks to guarantee loading state cleanup

**Other Improvements:**
- Renamed notify function parameters from `val` to `value` for consistency
- Removed unnecessary code comments and whitespace
- Maintained identical functionality - pure refactoring with no behavior changes

No functional changes - purely organizational improvements for better code readability and maintainability.
Improvements to ConnectionOptions:
- Add keyboard support (Enter key) for port and remote URL inputs
- Refactor port refresh logic into dedicated method

Improvements to RemoteAuthentication:
- Add email validation with real-time button state
- Add password length validation for login/signup
- Enhance visual feedback with success states
- Improve styling consistency and spacing
- Clear auth state when navigating back
- Remove stale comments

Core improvements:
- Refactor temp client creation into withTempClient helper with proper resource cleanup
- Extract buildAppInfo helper to reduce duplication
- Simplify remote auth methods using new helper
- Improve error handling throughout

Minor cleanup:
- Better state organization with comments in web.ts
- Remove extra whitespace in utils.ts
- Remove @coasys/ad4m-connect dependency from helpers and react hooks packages
- Update getProfile to accept AgentClient parameter instead of internally fetching client
- Update useAgent hook to pass client to getProfile calls
- Fix import organization and remove unused imports across hooks packages
- Update workspace dependencies to use workspace:* protocol
- Add @types/uuid as devDependency to helpers package
- Clean up circular import references in connect utils
- Update comment from "TODO: remove" to "Re-export isEmbedded utility"

This removes the circular dependency between ad4m-connect and hooks packages,
allowing views to use hooks without bundling ad4m-connect multiple times.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

Reworks Ad4m connect into an embedded, event-driven core with postMessage AD4M_CONFIG intake and local persistence; replaces legacy UI with LitElement views and shared styles; removes many legacy UI components and Electron bridge; updates hooks (removing useClient/useEntries) and helper APIs (getProfile signature); updates package manifests and build scripts.

Changes

Cohort / File(s) Summary
Package manifests & build
ad4m-hooks/helpers/package.json, ad4m-hooks/react/package.json, ad4m-hooks/vue/package.json, connect/package.json, connect/scripts/esbuild_index.js, core/package.json, ui/package.json
Version bumps; switched several deps to workspace:*; removed dist from files; added @types/uuid dev dep; added/expanded publishConfig; added build:index and esbuild bundler; updated exports/types in connect.
Hooks removed / public exports changed
ad4m-hooks/react/src/useClient.ts, ad4m-hooks/react/src/useEntries.ts, ad4m-hooks/react/src/index.ts, ad4m-hooks/vue/src/useClient.ts, ad4m-hooks/vue/src/index.ts
Deleted React hooks useClient and useEntries; removed Vue useClient export; adjusted export lists (e.g., removed useClient, added useModel).
Helpers & small call-site updates
ad4m-hooks/helpers/src/getProfile.ts, ad4m-hooks/helpers/src/factory/SubjectRepository.ts, ad4m-hooks/react/src/useAgent.ts, ad4m-hooks/react/src/useMe.ts, ad4m-hooks/vue/src/useAgent.ts, ad4m-hooks/vue/src/useMe.ts, ad4m-hooks/vue/src/usePerspectives.ts
getProfile signature changed to accept AgentClient as first param; minor type annotations, import consolidations, and guard/formatting tweaks; updated call sites to pass client.
Connect core, types & utils (rewrite)
connect/src/core.ts, connect/src/types.ts, connect/src/utils.ts, connect/src/index.ts
Major rewrite: Ad4mConnect becomes EventTarget-based with embedded lifecycle, AD4M_CONFIG postMessage intake, local storage helpers; new public methods (connect, isValidAd4mAPI, isMultiUser, submitEmail, verifyEmailCode, loginWithPassword, createAccount); moved/added types and top-level getAd4mConnect/getAd4mClient APIs.
UI rework & shared styles
connect/src/web.ts, connect/src/styles/shared-styles.ts
Replaced monolithic UI with core-driven Ad4mConnectUI(core) returning a connect element; introduced shared LitElement CSS and modal multi-view orchestration.
Removed legacy UI components & Electron
connect/src/components/* (many deleted: e.g., ConnectionOverview.ts, MultiUserAuth.ts, RemoteConnection.ts, Settings.ts, Start.ts, ScanQRCode.ts, etc.), connect/src/electron.ts
Removed ~19 legacy Lit components and the Electron bridge module; verify remaining code does not import deleted modules.
New views & icons (LitElements)
connect/src/components/views/ConnectionOptions.ts, connect/src/components/views/LocalAuthentication.ts, connect/src/components/views/RemoteAuthentication.ts, connect/src/components/views/CurrentState.ts, connect/src/components/icons/*, connect/src/components/icons/index.ts
Added ConnectionOptions, LocalAuthentication, RemoteAuthentication, CurrentState views; ~10 new SVG icon components and an icons index re-exporting them.
Small edits & formatting
ad4m-hooks/helpers/src/factory/SubjectRepository.ts, assorted hook files
Minor type annotation, parameter typing, import consolidation, and small guard/formatting tweaks across several files.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Parent as ParentWindow
  participant Core as Ad4mConnect
  participant Client as Ad4mClient
  participant UI as Ad4mConnectUI

  Note over Parent,Core: Embedded host posts AD4M_CONFIG
  Parent->>Core: postMessage(AD4M_CONFIG {url, port, token})
  Core->>Core: validate origin & persist config (setLocal)
  Core->>Client: buildClient() (ws/graphql)
  Client-->>Core: client ready
  Core->>UI: dispatch authstatechange / connection events
  UI->>Core: user triggers auth flows (submitEmail / verifyEmailCode / login / createAccount)
  Core->>Core: withTempClient(...) for verification
  Core->>Client: perform auth action
  Client-->>Core: auth result / token
  Core->>Core: persist token (setLocal) and resolve connect()
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇
I hopped through configs, munched the old UI vines,
stitched icons and views and learned new message lines.
"Post me your config," I twitched with eager ears—
the core now listens softly, saving ports and peers.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Ad4m connect updates' is vague and overly broad, failing to convey the significant architectural refactoring and consolidation work that is the core of this changeset. Consider a more specific title like 'Refactor Ad4m connect: consolidate 19 components to 4 views and extract business logic to core' to better reflect the primary change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

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

⚠️ Outside diff range comments (6)
ad4m-hooks/vue/src/usePerspectives.ts (1)

37-42: Pre-existing bug: removeListener used instead of addListener, and wrong callback array referenced.

This appears to be a copy-paste error from the "link-added" handler above:

  1. removeListener should likely be addListener to register for "link-removed" events
  2. onAddedLinkCbs should be onRemovedLinkCbs

This is not introduced by this PR, but worth fixing while you're in this file.

Proposed fix
-  p.removeListener("link-removed", (link) => {
-    onAddedLinkCbs.value.forEach((cb) => {
+  p.addListener("link-removed", (link) => {
+    onRemovedLinkCbs.value.forEach((cb) => {
       cb(p, link);
     });
     return null;
   });
ad4m-hooks/vue/src/useMe.ts (1)

27-39: Inconsistent reference between agent.value and newAgent in watch handler.

The outer condition checks agent.value?.perspective while the inner code extracts from newAgent?.perspective. Since newAgent is the callback parameter representing the new value, the outer condition should also use newAgent for consistency:

Proposed fix
   watch(
     () => agent.value,
     (newAgent) => {
-      if (agent.value?.perspective) {
+      if (newAgent?.perspective) {
         const perspective = newAgent?.perspective;
         if (!perspective) return;
         profile.value = formatter(perspective.links);
       } else {
         profile.value = null;
       }
     },
     { immediate: true }
   )

With this change, the inner null-check on line 32 becomes redundant and can be removed since the outer condition already guards for it.

ad4m-hooks/react/src/useAgent.ts (2)

27-41: Missing null check and error handling for getProfile call.

The getProfile function can return undefined if no agent perspective is found (per the function signature in getProfile.ts). Accessing profile.perspective.links without a null check will throw a runtime error.

Proposed fix with null safety and error handling
   const getData = useCallback(() => {
     if (didRef) {
         if (props.formatter) {
-          getProfile(props.client, didRef).then(profile => setProfile(props.formatter(profile.perspective.links)))
+          getProfile(props.client, didRef)
+            .then(profile => {
+              if (profile?.perspective?.links) {
+                setProfile(props.formatter(profile.perspective.links));
+              }
+            })
+            .catch((error) => setError(error.toString()));
         }
 
       props.client

41-41: Incomplete dependency array in useCallback.

The getData callback references didRef, props.formatter, props.client, and mutate, but the dependency array only includes cacheKey. This can cause stale closures if these values change.

Proposed fix
-  }, [cacheKey]);
+  }, [cacheKey, didRef, props.client, props.formatter, mutate]);
ad4m-hooks/helpers/src/getProfile.ts (1)

14-18: Non-null assertions may cause runtime errors.

Line 15 uses agentPerspective!.perspective!.links with non-null assertions, but perspective could be null or undefined based on the Agent type from the byDID API. This would cause a runtime error.

Consider adding a null check for perspective before accessing links.

Suggested fix
   if (agentPerspective) {
-    const links = agentPerspective!.perspective!.links;
+    const links = agentPerspective.perspective?.links;
+    if (!links) return agentPerspective;

     if (formatter) {
       return formatter(links);
     }

     return agentPerspective
   }
ad4m-hooks/react/src/useMe.ts (1)

58-77: Memory leak: Listeners are never removed.

The addAgentStatusChangedListener and addUpdatedListener subscriptions are set up but never cleaned up. The TODO on line 75 acknowledges this, but this will cause memory leaks and stale callback invocations when the component unmounts or when agent changes.

Additionally, the dependency array [agent] is incomplete—data and mutate are used inside the callback but not listed, which can cause stale closure issues.

If the AD4M client API supports removing listeners, the cleanup should be added. Would you like me to help implement the cleanup once the listener removal API is available?

#!/bin/bash
# Description: Check if AgentClient has methods to remove listeners
# Expected: Find removeListener or unsubscribe methods

ast-grep --pattern $'class AgentClient {
  $$$
  remove$_Listener($_) {
    $$$
  }
  $$$
}'
🤖 Fix all issues with AI agents
In `@ad4m-hooks/helpers/src/getProfile.ts`:
- Around line 4-7: Remove the unused exported interface Payload by deleting the
declaration "export interface Payload { url: string; perspectiveUuid: string; }"
from the file; ensure no other references to the symbol Payload remain in the
codebase (if found, either import/use it or remove those references) and run a
quick build/typecheck to confirm no breakages.

In `@ad4m-hooks/react/src/useEntries.ts`:
- Around line 1-3: Delete the duplicate module useEntries.ts (which currently
mirrors useMe.ts) to avoid redundancy; remove the file and any imports/exports
referencing useEntries.ts so only useMe.ts remains exported (update index.ts if
it re-exports useEntries) and ensure no code references functions or hooks from
useEntries.ts remain in the codebase.

In `@connect/package.json`:
- Around line 13-17: The root package export object currently specifies "types",
"import", and "default" but omits an explicit "require" condition; update the
root export in package.json (the object containing "types", "import", "default")
to add a "require" field that points to the CJS build (e.g., "./dist/index.cjs"
or the project's CJS entry) so it matches the other exports that explicitly
define "require" and ensures consistent CJS resolution.

In `@connect/scripts/esbuild_index.js`:
- Line 11: Replace the deprecated boolean watch option with the esbuild context
API: create a build context via esbuild.context(...) using the existing build
options (entryPoints, bundle, format, minify, sourcemap, outfile), then if
process.env.NODE_ENV === "dev" call await ctx.watch() to enable watching,
otherwise call await ctx.dispose() to run a one-time build; update references in
connect/scripts/esbuild_index.js where the watch property was used and ensure
ctx is awaited and disposed appropriately.

In `@connect/src/components/views/ConnectionOptions.ts`:
- Around line 10-11: The port property is declared with `@property`({ type: String
}) which coerces attributes to strings and can break numeric logic; update the
decorator on the port field (symbol: port in the ConnectionOptions
component/class) to use `@property`({ type: Number }) so attribute values are
converted to numbers, and ensure the TypeScript type remains number (port:
number) and any code that reads this property expects a numeric value.
- Around line 160-167: The input handler is assigning NaN into the component
state (this.newPort) when the field is empty/invalid; update the handler in the
ConnectionOptions component so you parse the value into a const (e.g., const
parsed = parseInt(input.value, 10)), check Number.isNaN(parsed) and only assign
this.newPort = parsed when valid, otherwise set this.newPort to undefined/null
(or leave previous value) and update the .value binding for the input to render
an empty string when newPort is undefined (e.g., .value=${this.newPort != null ?
this.newPort.toString() : ''}) to prevent "NaN" from appearing and avoid
ws://localhost:NaN.

In `@connect/src/components/views/RemoteAuthentication.ts`:
- Around line 75-89: The dispatched events in emailLogin, verifyEmailCode,
passwordLogin, and createAccount use this.email raw (which may include
trailing/leading spaces); normalize by trimming once (e.g., const email =
this.email?.trim()) before building event detail and use that trimmed email in
the CustomEvent payloads (keep other fields like password/password code
unchanged) and ensure any call-sites relying on isValidEmail remain compatible.

In `@connect/src/core.ts`:
- Around line 250-254: In requestCapability(invalidateToken = false) the code
sets this.token = null but doesn't clear persisted storage and uses null (not a
string); change it to set this.token = '' (empty string), call
localStorage.removeItem('token') (or localStorage.setItem('token', '') if your
persistence expects a key) to clear the persisted token, and then call
this.notifyConfigChange("token", this.token) so in-memory and persisted state
stay consistent; locate this in the requestCapability method and update the
token handling there.
- Around line 199-231: Guard the AD4M_CONFIG message handler by verifying the
sender: inside the window.addEventListener('message', ...) callback for
AD4M_CONFIG, first check that event.source === window.parent and that
event.origin is present in the allowlist provided via the instance options
(reference Ad4mConnectOptions or this.options.allowedOrigins); if either check
fails, return early and optionally log/ignore the message. Only after passing
these validations proceed to extract { port, token }, set
this.port/this.token/this.url, persist to localStorage, buildClient() and
checkAuth(); if the message is rejected and an embeddedReject exists, call it
with an appropriate error to surface the failure.

In `@connect/src/styles/shared-styles.ts`:
- Around line 55-121: The stylesheet currently removes the default focus
indicator with "outline: none" on the base button rule; add an accessible
replacement by adding a :focus-visible (and :focus-visible:not(:disabled)) rule
that provides a visible focus indicator (e.g., a clear outline or high-contrast
box-shadow and outline-offset) for all button variants (base button, and ensure
it applies to button.primary, button.secondary, button.ghost, button.link); keep
"outline: none" if you prefer but ensure the :focus-visible rule visually
indicates focus and matches the existing hover/disabled logic so keyboard users
regain a clear focus state.

In `@connect/src/web.ts`:
- Around line 152-158: Wrap the await this.core.connect() call in
connectLocalNode in a try/catch to prevent unhandled promise rejections and
surface UI errors the same way connectedCallback does; on catch, run the same
error-handling path used by connectedCallback (e.g., log the error and update
this.currentView to the error UI or invoke the connectedCallback error handler)
so failures are displayed to the user and not left unhandled.
- Around line 87-92: Replace the non-semantic clickable <div> that renders the
settings control (the element using class "settings-button") with a native
<button type="button">, add an accessible aria-label (e.g., aria-label="Open
settings"), and remove any role="button" or manual keyboard handlers that
duplicate native behavior; keep the existing onClick handler. Also update the
.settings-button CSS to include button-reset styles (e.g., appearance: none;
border: none; background: transparent; padding: 0; cursor: pointer;) while
preserving the existing positioning and color so visual layout is unchanged.
- Around line 101-103: The MutationObserver watching attributes (created in the
factory function) never sees Lit `@state`() changes like modalOpen, so remove/stop
using that observer and instead use the existing authstatechange event listener
to reveal the modal: in the authstatechange handler set this.modalOpen = true
(and set this.currentView = "connection-options" or the appropriate view) when
auto‑connect fails, and ensure the component listens for and handles
authstatechange on the element (or re-dispatches it from the auth logic) so the
modal toggles via the event rather than relying on attribute mutations; keep
modalOpen as `@state`() and do not try to detect it via MutationObserver.
🧹 Nitpick comments (12)
ad4m-hooks/helpers/src/getProfile.ts (2)

1-2: Consolidate imports from the same module.

Both imports are from @coasys/ad4m and can be combined into a single import statement for cleaner code.

Suggested fix
-import { AgentClient } from "@coasys/ad4m";
-import { LinkExpression } from "@coasys/ad4m";
+import { AgentClient, LinkExpression } from "@coasys/ad4m";

9-9: Tighten return type and handle the implicit undefined case.

The return type Promise<T | any> undermines the generic's purpose since any absorbs all types. Additionally, when agentPerspective is falsy, the function returns undefined implicitly, but the signature doesn't reflect this.

Consider:

  • Replacing any with the actual return type (likely Agent or a perspective type)
  • Adding | undefined to the return type to indicate the function may not return a value
ad4m-hooks/react/src/useMe.ts (1)

83-88: Unnecessary conditional check for formatter.

The formatter parameter is required in the function signature (line 19), so the if (formatter) check on line 84 is always truthy and can be removed.

Suggested simplification
   if (perspective) {
-    if (formatter) {
-        profile = formatter(perspective.links)
-    }
- 
+    profile = formatter(perspective.links);
   }
connect/scripts/esbuild_index.js (1)

13-13: Consider logging the build error before exiting.

The .catch(() => process.exit(1)) swallows errors silently, making debugging difficult when builds fail.

🔧 Suggested improvement
-.catch(() => process.exit(1));
+.catch((error) => {
+  console.error("Build failed:", error);
+  process.exit(1);
+});
connect/src/components/icons/LocalIcon.ts (1)

5-8: Consider adding explicit stroke-width for consistent rendering.

The outer circle relies on the browser's default stroke-width, which may vary. Adding an explicit value ensures consistent appearance.

🔧 Suggested improvement
-    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
-      <circle cx="12" cy="12" r="10"></circle>
+    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+      <circle cx="12" cy="12" r="10"></circle>
connect/src/components/icons/RemoteIcon.ts (1)

5-9: Add explicit stroke-width for consistency with other icons.

Similar to LocalIcon, this SVG lacks an explicit stroke-width. For visual consistency across the icon set, consider adding stroke-width="2".

🔧 Suggested improvement
-    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
+    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
connect/src/components/icons/RefreshIcon.ts (1)

5-8: Consider removing hardcoded width and height for flexible sizing.

Unlike the other icon components that rely solely on viewBox, this icon has hardcoded width="16" height="16". This can interfere with CSS-based resizing. Let the parent container control dimensions.

🔧 Suggested improvement
-    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
+    <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
connect/src/components/views/LocalAuthentication.ts (1)

8-10: Align capabilities type with capSentence input

capSentence expects a capability object (not a plain string). Typing capabilities as CapabilityInput[] keeps TS aligned with runtime usage and avoids accidental misuse.

🔧 Proposed fix
-import { capSentence } from "@coasys/ad4m";
+import { capSentence, CapabilityInput } from "@coasys/ad4m";
@@
-  `@property`({ type: Array }) capabilities: string[] = [];
+  `@property`({ type: Array }) capabilities: CapabilityInput[] = [];

Also applies to: 162-164

connect/src/components/views/ConnectionOptions.ts (1)

240-262: Remove commented-out UI blocks

The commented sections at the end add noise and tend to rot. Consider deleting or moving them to an issue/ADR.

connect/src/utils.ts (1)

31-31: Type the connectWebSocket API

The url parameter is implicit any. Adding explicit types improves TS strictness and API clarity.

🔧 Proposed fix
-export async function connectWebSocket(url, timeout = 10000) {
+export async function connectWebSocket(url: string, timeout = 10000): Promise<WebSocket> {
connect/src/core.ts (1)

76-88: Avoid async Promise executor in connect()

new Promise(async ...) is discouraged and can mask errors. Since connect() is already async, a direct try/catch is clearer and matches idiomatic JS.

🔧 Proposed fix
-    return new Promise(async (resolve, reject) => {
-      try {
-        await connectWebSocket(this.url);
-        setLocal("ad4m-url", this.url);
-        this.ad4mClient = await this.buildClient();
-        await this.checkAuth();
-        resolve(this.ad4mClient);
-      } catch (error) {
-        console.error('[Ad4m Connect] Connection failed:', error);
-        this.notifyConnectionChange("error");
-        reject(error);
-      }
-    });
+    try {
+      await connectWebSocket(this.url);
+      setLocal("ad4m-url", this.url);
+      this.ad4mClient = await this.buildClient();
+      await this.checkAuth();
+      return this.ad4mClient;
+    } catch (error) {
+      console.error('[Ad4m Connect] Connection failed:', error);
+      this.notifyConnectionChange("error");
+      throw error;
+    }
connect/src/components/icons/ArrowLeftRightIcon.ts (1)

5-6: Consider marking the SVG as decorative (or labeling it).
If this icon is purely decorative, add aria-hidden="true" and focusable="false" to prevent screen readers from announcing it. If it conveys meaning, consider exposing an aria-label prop.

♿️ Suggested tweak
-    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-right" viewBox="0 0 16 16">
+    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-right" viewBox="0 0 16 16" aria-hidden="true" focusable="false">

- Remove unused Payload interface from getProfile.ts
- Delete duplicate useEntries.ts file
- Add CJS support to package.json exports with require field
- Update esbuild script to use modern context API for dev/prod builds
- Fix ConnectionOptions port property type (String → Number)
- Add NaN validation to port input with Number.isFinite check
- Trim email addresses in all RemoteAuthentication event dispatchers
- Add postMessage origin validation for embedded mode security
  - Verify event source is parent window
  - Check allowedOrigins array when configured
- Fix token invalidation to use empty string and clear localStorage
- Add :focus-visible indicators for all button variants (accessibility)
- Replace non-semantic div with semantic button for settings control
- Add error handling to connectLocalNode method
- Remove broken MutationObserver watching @State() changes
- Update visibility handling via authstatechange events
- Add allowedOrigins security option to Ad4mConnectOptions type
- Update pnpm-lock.yaml with workspace links for monorepo consistency
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@connect/src/components/views/RemoteAuthentication.ts`:
- Around line 70-73: The back() method resets the view but leaves sensitive
fields populated; update the RemoteAuthentication class's back() method to also
clear local sensitive state by setting password and emailSecurityCode to
null/empty (or their initial values) before or after setting remoteAuthState to
null, ensuring both fields are reset when the user navigates back so the
password step does not repopulate stale data.

In `@connect/src/core.ts`:
- Around line 385-395: The notifyAuthChange method only resolves the embedded
connect promise on "authenticated"; update it to also reject immediately when
auth transitions to failing states by calling embeddedReject with a clear Error
(e.g., message indicating "unauthenticated" or "locked") and then clearing
embeddedResolve and embeddedReject; modify notifyAuthChange to check for value
=== "unauthenticated" || value === "locked" (or other failing AuthStates) and if
embeddedReject exists invoke it and clear both fields so embedded connect() does
not wait for the 30s timeout; keep the existing authenticated-resolution
behavior intact.
🧹 Nitpick comments (2)
connect/src/core.ts (1)

76-89: Avoid async Promise executors in connect()

Line 76–89 uses new Promise(async ...), which is a common JS anti-pattern and flagged by lint. Since connect() is already async, you can return directly and let try/catch handle errors.

♻️ Suggested refactor
-    return new Promise(async (resolve, reject) => {
-      try {
-        await connectWebSocket(this.url);
-        setLocal("ad4m-url", this.url);
-        this.ad4mClient = await this.buildClient();
-        await this.checkAuth();
-        resolve(this.ad4mClient);
-      } catch (error) {
-        console.error('[Ad4m Connect] Connection failed:', error);
-        this.notifyConnectionChange("error");
-        reject(error);
-      }
-    });
+    try {
+      await connectWebSocket(this.url);
+      setLocal("ad4m-url", this.url);
+      this.ad4mClient = await this.buildClient();
+      await this.checkAuth();
+      return this.ad4mClient;
+    } catch (error) {
+      console.error('[Ad4m Connect] Connection failed:', error);
+      this.notifyConnectionChange("error");
+      throw error;
+    }
connect/src/web.ts (1)

11-11: Avoid deep import of VerificationRequestResult

Line 11 imports from @coasys/ad4m/lib/src/..., which is brittle against package export changes. Prefer the public entry (as used in RemoteAuthentication.ts) if available.

♻️ Suggested change
-import { VerificationRequestResult } from "@coasys/ad4m/lib/src/runtime/RuntimeResolver";
+import { VerificationRequestResult } from "@coasys/ad4m";

…lows

Security improvements:
- Clear sensitive fields (password, emailSecurityCode) in RemoteAuthentication.back()
  to prevent data persistence when navigating back
- Add immediate promise rejection in embedded mode for failed auth states
  (unauthenticated/locked) to avoid 30-second timeout wait

Authentication flow fixes:
- Reject embedded connect() promise immediately on auth failure with clear error messages
- Clean up promise handlers (embeddedResolve/embeddedReject) after rejection
- Preserve existing authenticated-resolution behavior in embedded mode
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@connect/src/components/views/RemoteAuthentication.ts`:
- Around line 94-106: The onEmailSecurityCodeChange handler should avoid
triggering verifyEmailCode while a verification request is already in flight:
add a guard that checks the component state/flag remoteAuthLoading and only
calls verifyEmailCode() when remoteAuthLoading is false (e.g., if
(this.emailSecurityCode.length === 6 && !this.remoteAuthLoading)
this.verifyEmailCode()); update any related logic around emailSecurityCode and
remoteAuthLoading to ensure the flag is set before/after the async
verifyEmailCode call so duplicates are prevented.

In `@connect/src/core.ts`:
- Around line 116-128: The closed handler can get an already-authenticated but
dead embedded client because this.connect() may return the existing ad4mClient
without verifying the underlying WS is alive; change the logic so when the
CloseEvent indicates an unexpected close (event.wasClean false and
!this.requestedRestart) you verify the returned/client's socket liveness before
accepting it: either update connect(clientOptions) to accept a
forceFresh/forceReconnect flag and call connect({forceFresh:true}) from the
closed handler, or after getting a client check a liveness property/method
(e.g., client.socket.readyState or client.isConnected()) and if it's not open,
discard it and create a new client (rebuild) so the closed WS is replaced; keep
notifyConnectionChange("error") behavior if reconnect fails.
- Around line 43-89: The connect() method currently uses a new Promise with an
async executor for the standalone path which triggers the noAsyncPromiseExecutor
lint; refactor the standalone branch to use the async function's natural flow:
remove new Promise(async (resolve, reject) => { ... }), perform the operations
sequentially (await connectWebSocket(this.url); setLocal("ad4m-url", this.url);
this.ad4mClient = await this.buildClient(); await this.checkAuth();) inside a
try block and return this.ad4mClient, and in the catch block call
this.notifyConnectionChange("error"), console.error the error and rethrow it;
keep the existing embedded-mode Promise logic (with
embeddedResolve/embeddedReject and ad4mClient/authState check) unchanged.
🧹 Nitpick comments (1)
connect/src/components/views/RemoteAuthentication.ts (1)

135-139: Add autocomplete hints to improve UX and password manager support.

✨ Suggested additions
               <input
                 type="email"
+                autocomplete="email"
                 placeholder="email@example.com"
                 .value=${this.email || ""}
@@
               <input
                 type="text"
                 maxlength="6"
                 inputmode="numeric"
                 pattern="[0-9]{6}"
+                autocomplete="one-time-code"
                 placeholder="000000"
                 .value=${this.emailSecurityCode || ""}
@@
               <input
                 type="password"
+                autocomplete="current-password"
                 placeholder="Password..."
                 .value=${this.password || ""}
@@
               <input
                 type="password"
+                autocomplete="new-password"
                 placeholder="Password..."
                 .value=${this.password || ""}

Also applies to: 181-187, 215-218, 255-258

…nditions

Connection improvements:
- Refactor connect() standalone path to use natural async/await flow
  instead of async promise executor (fixes noAsyncPromiseExecutor lint)
- Fix WebSocket reconnection to force fresh client rebuild via buildClient()
  instead of calling connect() which could return stale embedded client
- Add proper error handling and logging to reconnection attempts

Race condition fixes:
- Add guard to prevent duplicate email verification requests when code
  is auto-submitted (check !remoteAuthLoading before calling verifyEmailCode)
- Prevents multiple concurrent verification attempts during rapid input

Code quality:
- Remove Promise constructor anti-pattern in standalone connect()
- Use direct return/throw instead of resolve/reject for cleaner flow
- Preserve all existing embedded mode Promise logic unchanged
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@connect/src/core.ts`:
- Line 23: The activeSocket property is declared as type WebSocket but
initialized to null; update its declaration so its type explicitly allows null
(make activeSocket nullable) to match its initialization and other nullable
properties (e.g., ad4mClient?, wsClient?), then run type checks; locate the
activeSocket symbol in the class in connect/src/core.ts and change its type
annotation to permit null and keep the initializer as null.
🧹 Nitpick comments (5)
connect/src/components/views/RemoteAuthentication.ts (3)

32-34: Consider responsive widths for smaller viewports.

Fixed widths (width: 280px and width: 194px) may cause horizontal overflow on narrow screens. If this component is expected to render in mobile contexts, consider using max-width with a percentage fallback.

Also applies to: 48-56


217-226: Consider adding autocomplete attributes to password inputs.

Adding autocomplete="current-password" (line 218) and autocomplete="new-password" (line 258) helps password managers distinguish between login and signup flows, improving UX and accessibility.

♻️ Suggested enhancement
              <input
                type="password"
+               autocomplete="current-password"
                placeholder="Password..."
                .value=${this.password || ""}
              <input
                type="password"
+               autocomplete="new-password"
                placeholder="Password..."
                .value=${this.password || ""}

Also applies to: 257-266


137-145: Consider adding autocomplete="email" for better autofill support.

This helps browsers and password managers auto-populate the email field.

♻️ Suggested enhancement
              <input
                type="email"
+               autocomplete="email"
                placeholder="email@example.com"
connect/src/core.ts (2)

154-165: Ensure Apollo client is stopped in withTempClient.

wsClient.dispose() alone can leave Apollo watchers hanging. Stop the Apollo client in finally.

♻️ Proposed refactor
-    } finally {
-      wsClient.dispose();
-    }
+    } finally {
+      apolloClient.stop();
+      wsClient.dispose();
+    }

196-258: Prefer scoped targetOrigin when requesting config.

If allowedOrigins is provided, avoid broadcasting to "*" and send to those origins instead.

🔒 Proposed refinement
-    window.parent.postMessage({ type: 'REQUEST_AD4M_CONFIG' }, '*');
+    const targets = (this.options.allowedOrigins && this.options.allowedOrigins.length > 0)
+      ? this.options.allowedOrigins
+      : ['*'];
+    for (const origin of targets) {
+      window.parent.postMessage({ type: 'REQUEST_AD4M_CONFIG' }, origin);
+    }

Change activeSocket property type from `WebSocket = null` to
`WebSocket | null = null` to properly reflect its nullable nature
and align with TypeScript strict null checking. This matches the
pattern used for other optional properties (ad4mClient?, wsClient?,
apolloClient?) and fixes the type mismatch error.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@connect/src/core.ts`:
- Around line 290-301: verifyLocalAd4mCode currently uses this.requestId! which
can be undefined if requestCapability wasn't called; add a guard at the start of
verifyLocalAd4mCode to check that this.requestId is present (and not use the
non-null assertion), return false or throw a clear error and log a message if
it's missing, and only call ad4mClient.agent.generateJwt when requestId exists;
reference the verifyLocalAd4mCode method, the requestId property, and the
requestCapability flow to ensure callers must obtain a requestId before
verifying.
- Around line 196-258: The initializeEmbeddedMode listener currently uses
event.data.port and event.data.token directly; validate and normalize these
before persisting or using them: inside the AD4M_CONFIG handler (in
initializeEmbeddedMode) ensure event.data.port is present and can be parsed to a
positive integer (e.g., parseInt and isFinite check) and that token, if present,
is a string (or treat missing token as undefined), and if validation fails call
this.embeddedReject(new Error(...)) and return; only call
setLocal('ad4m-port'...), setLocal('ad4m-token'...), setLocal('ad4m-url'...) and
set this.port/this.token/this.url after successful normalization, then proceed
to await this.buildClient() and this.checkAuth(); also avoid storing the literal
string "undefined" by gating setLocal calls on validated values.

- Add validation and normalization for AD4M_CONFIG port and token fields
  - Validate port is present and can be parsed to positive integer
  - Normalize token as string or empty string, avoid persisting undefined
  - Reject embedded promise with clear errors on validation failure
- Add guard to verifyLocalAd4mCode requiring requestId before use
  - Remove non-null assertion operator for type safety
  - Return false with descriptive error if requestCapability() not called
- Extract embedded promise handlers into reusable helper methods
  - Add resolveEmbedded() and rejectEmbedded() to eliminate repetition
  - Consolidate cleanup logic (clear both resolve/reject handlers)
  - Simplify 6 call sites throughout initializeEmbeddedMode flow
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@connect/src/core.ts`:
- Around line 196-272: In initializeEmbeddedMode(): when processing AD4M_CONFIG,
detect when rawToken is absent or invalid (normalizedToken === '') and
explicitly clear any persisted token instead of leaving a stale value; replace
or augment the current conditional that only writes setLocal('ad4m-token',
normalizedToken) with logic that removes the stored token (e.g., call
removeLocal('ad4m-token') or delete the key) when normalizedToken is empty, and
only setLocal when a non-empty normalizedToken exists so reloads cannot reuse
stale credentials.

When processing AD4M_CONFIG with absent or invalid token, explicitly
remove any persisted token from localStorage to prevent stale
credentials from being reused on reload. Previously only stored valid
tokens but left old values in place when normalizedToken was empty.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@connect/src/core.ts`:
- Around line 43-73: The embedded connect() path currently only resolves
immediately when this.ad4mClient exists and this.authState === 'authenticated',
but if this.ad4mClient exists and auth has already failed (this.authState is
'unauthenticated' or 'locked') the promise stays waiting until timeout; fix by
adding an early rejection branch: after the existing check for this.ad4mClient
&& this.authState === 'authenticated', add an else-if that checks
this.ad4mClient && this.authState !== 'authenticated' and immediately
clearTimeout(timeout) and invoke this.embeddedReject(new Error(`Embedded auth
state: ${this.authState}`)) (or call reject(...) directly) so the connect()
promise rejects immediately when prior auth failed instead of waiting 30s;
reference symbols: connect(), this.ad4mClient, this.authState,
this.embeddedReject, timeout.

…lure

In embedded mode, the connect() promise now checks if authentication
has already failed before connect() was called. Previously, if the
client existed but authState was 'locked' or 'unauthenticated', the
promise would wait for the full 30-second timeout. Now it rejects
immediately with a descriptive error, improving error feedback and
avoiding unnecessary delays when auth has already failed.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@connect/src/core.ts`:
- Around line 311-327: The four auth methods (verifyLocalAd4mCode,
verifyEmailCode, loginWithPassword, createAccount) obtain a token then call
this.connect() but never validate the token, which causes connect() to hang in
embedded mode; update each method to call await this.checkAuth() immediately
after setting this.token (and persisting it) and before calling await
this.connect() so checkAuth() validates the token and updates authState to
'authenticated' prior to connect().
♻️ Duplicate comments (1)
connect/src/core.ts (1)

72-78: Missing return after early rejection causes fallthrough to standalone mode.

When auth already failed in embedded mode (lines 72-76), the promise is rejected but execution continues past the if block to line 78, where it would fall through to the standalone connection logic. Add a return statement after the early rejection.

🐛 Proposed fix
         } else if (this.ad4mClient && this.authState !== 'authenticated') {
           // Auth already failed before connect() was called
           clearTimeout(timeout);
           reject(new Error(`Embedded auth state: ${this.authState}`));
+          return;
         }
       });
     }
🧹 Nitpick comments (2)
connect/src/core.ts (2)

101-102: Minor type inconsistency: assigning null to optional properties.

Properties wsClient and apolloClient are declared as optional (wsClient?: WSClient), meaning their type is WSClient | undefined. Assigning null technically introduces a type mismatch. While this works at runtime, consider using undefined for consistency.

♻️ Suggested fix
       this.wsClient.dispose();
       this.apolloClient.stop();
-      this.wsClient = null;
-      this.apolloClient = null;
+      this.wsClient = undefined;
+      this.apolloClient = undefined;
     }

186-197: Add type guard before accessing error.message.

In strict TypeScript, the catch parameter is typed as unknown. Accessing error.message directly (line 189) could fail if the caught value isn't an Error object. Consider adding a type guard.

♻️ Suggested fix
     } catch (error) {
       console.error('[Ad4m Connect] Authentication check failed:', error);
       const lockedMessage = "Cannot extractByTags from a ciphered wallet. You must unlock first.";
-      if (error.message === lockedMessage) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      if (errorMessage === lockedMessage) {
         // TODO: isLocked throws an error, should just return a boolean. Temp fix
         this.notifyAuthChange("locked");
         return true;

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@connect/package.json`:
- Around line 5-6: The package.json's "main" field points to dist/web.js (a Lit
web component) while "types" points to dist/index.d.ts (which exports functions
such as getAd4mConnect and getAd4mClient), causing a runtime/typings mismatch;
change the "main" entry to point to the CommonJS/ES module entry that matches
the type definitions (e.g., dist/index.js) so that runtime exports align with
the type exports (update "main": "dist/index.js" and verify dist/index.{js,d.ts}
export getAd4mConnect and getAd4mClient).
🧹 Nitpick comments (2)
connect/package.json (2)

40-41: Redundant tsc invocations in build pipeline.

The build script runs tsc five times: once in each of build:index, build:core, build:web, build:utils, and then again at the end. Since they all use the same tsconfig.json, this is wasteful. Consider running tsc once before the esbuild steps.

♻️ Suggested optimization
-    "build": "pnpm run build:index && pnpm run build:core && pnpm run build:web && pnpm run build:utils && tsc",
-    "build:index": "tsc --project tsconfig.json && node scripts/esbuild_index.js",
-    "build:core": "tsc --project tsconfig.json && node scripts/esbuild.js",
-    "build:web": "tsc --project tsconfig.json && node scripts/esbuild_web.js",
-    "build:utils": "tsc --project tsconfig.json && node scripts/esbuild_utils.js"
+    "build": "tsc --project tsconfig.json && pnpm run build:index && pnpm run build:core && pnpm run build:web && pnpm run build:utils",
+    "build:index": "node scripts/esbuild_index.js",
+    "build:core": "node scripts/esbuild.js",
+    "build:web": "node scripts/esbuild_web.js",
+    "build:utils": "node scripts/esbuild_utils.js"

62-68: Build-time dependencies in dependencies instead of devDependencies.

esbuild-plugin-copy and esbuild-plugin-replace are build-time tools used only by the esbuild scripts. They should be in devDependencies to avoid bloating the install for consumers.

♻️ Suggested fix

Move to devDependencies:

  "devDependencies": {
    "@coasys/ad4m": "workspace:*",
    "@apollo/client": "3.7.10",
    "@types/node": "^16.11.11",
    "esbuild": "^0.15.5",
+   "esbuild-plugin-copy": "^2.1.1",
    "esbuild-plugin-lit": "^0.0.10",
+   "esbuild-plugin-replace": "^1.4.0",
    "graphql-ws": "5.12.0",
    ...
  },
  "dependencies": {
    "@undecaf/barcode-detector-polyfill": "^0.9.15",
    "@undecaf/zbar-wasm": "^0.9.12",
    "auto-bind": "^5.0.1",
-   "esbuild-plugin-copy": "^2.1.1",
-   "esbuild-plugin-replace": "^1.4.0",
    "lit": "^2.3.1"
  },

Change main entry point from dist/web.js to dist/index.js to align with
the types field (dist/index.d.ts). The web.js build is a Lit web component
while index.js exports the core API functions (getAd4mConnect, getAd4mClient).

This ensures backward compatibility with older tools that only read the
main/types fields. Modern bundlers will continue to use the exports field
which was already correctly configured.
- Add loading and validation checks to all Enter key handlers in RemoteAuthentication
  to prevent duplicate submissions (email login, password login, account creation)
- Add validation checks to remote node connection Enter handler in ConnectionOptions
- Disable remote connection button when URL is empty
- Minor formatting cleanup in core.ts

These changes ensure Enter key behavior matches button disabled states across
all authentication and connection flows, preventing race conditions and improving
user experience.
- Add CurrentState view to display connection details (local/remote, port/URL, auth status)
- Implement disconnect() method in core to clean up connections and tokens
- Add page reload on disconnect to reset app state
- Fix settings button visibility after page refresh by calling requestUpdate()
- Refactor shared-styles to consolidate duplicate CSS rules and improve organization
- Replace option-header with box-header across ConnectionOptions for consistency
- Replace button-row with center utility class in LocalAuthentication
- Add danger button styles for disconnect action
- Organize CSS by grouping buttons, inputs, layout utilities, and state indicators

The disconnect flow now properly clears the AD4M client, disposes websockets,
removes authentication tokens, and reloads the page to ensure apps start fresh
when switching between different nodes or users.
…lay remote node option

- Add showMultiUserOption boolean property to ConnectionOptions component
- Conditionally render remote node connection UI based on multiUser option
- Pass multiUser option from core to ConnectionOptions in web.ts
- Enables hiding remote connection for single-user deployments
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@connect/src/components/views/ConnectionOptions.ts`:
- Around line 133-135: The external-download button in ConnectionOptions.ts uses
window.open without protection; update the `@click` handler on the Download AD4M
button (the element rendering DownloadIcon()) to open the URL with a new tab and
include noopener,noreferrer (e.g., call
window.open("https://github.com/coasys/ad4m/releases", "_blank",
"noopener,noreferrer") or replace the button with an <a> tag that has
target="_blank" and rel="noopener noreferrer") so the opened page cannot access
window.opener.

@jhweir jhweir merged commit 6985c99 into dev Jan 24, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant