Skip to content

Refactor authentication structure by removing 'auth' package#1

Open
voidtopixel wants to merge 9 commits intogs-layer:feat/auth-providerfrom
voidtopixel:feat/auth-provider
Open

Refactor authentication structure by removing 'auth' package#1
voidtopixel wants to merge 9 commits intogs-layer:feat/auth-providerfrom
voidtopixel:feat/auth-provider

Conversation

@voidtopixel
Copy link
Copy Markdown

@voidtopixel voidtopixel commented Apr 6, 2026

Auth Plugin — Quick Start

Structure

plugins/auth/
├── types.ts       — base AuthProvider contract (implement for any backend)
├── plugin.ts      — auth() plugin (registers provider into the app)
└── frontx/
    ├── provider.ts       — frontxAuthProvider() factory (auto-wires REST transport)
    ├── auth-rest-plugin.ts — bearer/cookie injection + 401 refresh+retry
    └── transport-utils.ts  — URL/origin credential helpers

Two layers: base (minimal contract, no transport assumptions) and frontx (session kinds + REST binding).


Basic Setup

// src/app/auth/provider.ts
import { frontxAuthProvider } from '@cyberfabric/react';

export const authProvider = frontxAuthProvider({
  login: async (input) => {
    await api.post('/auth/login', input.payload);
    return { type: 'none' };
  },
  logout: async () => {
    await api.post('/auth/logout');
    return { type: 'redirect', redirectUrl: '/login' };
  },
  getSession: async () => ({ kind: 'bearer', token: localStorage.getItem('token')! }),
  checkAuth: async () => ({ authenticated: !!localStorage.getItem('token') }),
});
// src/app/main.tsx
const app = createHAI3App({
  auth: { provider: authProvider },
});

That's it. Bearer tokens are auto-attached to every REST request. 401s trigger refresh() if provided.


Custom Identity Fields

Define your identity shape and the provider infers it everywhere:

// src/app/auth/provider.ts
import { frontxAuthProvider } from '@cyberfabric/react';

export const authProvider = frontxAuthProvider({
  login: async (input) => { /* ... */ return { type: 'none' }; },
  logout: async () => { /* ... */ return { type: 'none' }; },
  getSession: async () => ({ kind: 'bearer', token: '...' }),
  checkAuth: async () => ({ authenticated: true }),
  getIdentity: async () => ({
    id: '1',
    email: 'john@example.com',
    role: 'admin',           // custom field
    orgId: 'org-42',         // custom field
  }),
});

export type AppAuthProvider = typeof authProvider;

// One-time augmentation — narrows types globally
declare module '@cyberfabric/react' {
  interface AppRuntimeExtensions {
    authProvider: AppAuthProvider;
  }
}

After this, every hook and app.getAuthProvider() call sees your custom fields and methods with full autocomplete — no generics needed at call-sites.


React Hooks

import { useLogin, useLogout, useGetIdentity, useAuthProvider } from '@cyberfabric/react';

// Login
const login = useLogin();
await login({ type: 'password', payload: { email, password } });

// Logout
const logout = useLogout();
await logout();              // follows provider redirect
await logout('/goodbye');    // override redirect

// Identity (fetches on mount, reactive)
const { identity, isPending, error, refetch } = useGetIdentity();
// identity.role and identity.orgId are typed if augmented

// Direct provider access
const provider = useAuthProvider();
provider?.getSession();

Cookie Sessions

export const authProvider = frontxAuthProvider(
  {
    login: async (input) => { /* ... */ return { type: 'none' }; },
    logout: async () => { /* ... */ return { type: 'none' }; },
    getSession: async () => ({ kind: 'cookie', csrfToken: getCsrf() }),
    checkAuth: async () => ({ authenticated: true }),
  },
  {
    csrfHeaderName: 'X-CSRF-Token',
    allowedCookieOrigins: ['https://api.example.com'],
  },
);

Cookie sessions auto-set withCredentials: true for same-origin and allowlisted origins.


Extra Provider Methods

You can add any custom methods — they pass through with full types:

export const authProvider = frontxAuthProvider({
  login: async () => ({ type: 'none' }),
  logout: async () => ({ type: 'none' }),
  getSession: async () => null,
  checkAuth: async () => ({ authenticated: false }),

  // Custom methods
  requestPasswordReset: async (email: string) => { /* ... */ },
  verifyMfa: async (code: string) => { /* ... */ },
});

After augmenting AppRuntimeExtensions, these methods are visible on useAuthProvider() and app.getAuthProvider().

gs-layer and others added 9 commits March 31, 2026 22:55
Add @hai3/auth package (L1 SDK, types only) defining the AuthProvider
contract with support for bearer tokens, cookie-sessions, and custom
auth mechanisms via the AuthSession.kind discriminant.

Add auth() framework plugin that wires any AuthProvider into the
@hai3/api REST transport layer:
- Bearer header attachment on every request
- Cookie-session with withCredentials and optional CSRF header
- Automatic 401 refresh+retry with concurrent refresh deduplication
- Pluggable transport binder (default: hai3ApiTransport)
- app.auth runtime surface via new generic provides.app mechanism

Enhance @hai3/api REST protocol:
- RestRequestOptions (params, signal, withCredentials) with
  backward-compatible overloads
- AbortSignal propagation through plugin context and axios
- axios.isCancel bypass of onError chain
- Retry preserves plugin-modified request context

Additional fixes:
- MockEventSource async open and abort handling
- BaseApiService.registerPlugin test for unregistered protocol
- isMockPlugin test for inherited MOCK_PLUGIN symbol
- ESLint config: exclude .artifacts and .agents directories

Update AI guidelines: add AUTH.md target, document auth plugin in
FRAMEWORK.md, document RestRequestOptions in API.md.
Resolve conflicts from @hai3 -> @cyberfabric package rename:
- Rename @hai3/auth to @cyberfabric/auth across all auth files
- Update peer dependencies to @cyberfabric/* namespace
- Update build scripts with @cyberfabric/auth workspace
- Regenerate package-lock.json from upstream base
- Exclude packages/auth/ from cypilot spec-coverage (types-only,
  no runtime logic to trace)
- Add @cpt- scope and block markers to auth.ts plugin
- Move @cyberfabric/auth from dependencies to peerDependencies
  (optional: true) in framework — auth is opt-in and not yet
  published to npm, so it must not be a required transitive dep
…overage

Markers referenced spec IDs not defined in any artifact (code-orphan-ref).
Exclude auth.ts from coverage scanning until a backing CDSL artifact is
created. Auth package already excluded.
Blocking:
- Fix protocol-relative URL credential leak (//host treated as relative)
- Add HEAD/OPTIONS to AuthTransportRequest method union and
  isSupportedAuthTransportMethod so onTransportError fires for all methods
- Filter opaque "null" origin in getRuntimeOrigin (sandboxed iframe/file://)

Recommended:
- Wrap refresh promise await in try/catch — concurrent waiters no longer
  receive unhandled rejections when refresh fails
- Document SSE auth as out-of-scope in auth() plugin JSDoc
- Add explicit comments for custom session kind pass-through (intentional)

Tests added (5 new, 48 total):
- Protocol-relative URL does not receive withCredentials
- Opaque "null" origin does not match URL origins
- Custom session: request unmodified, no retry after refresh
- Refresh rejection: all concurrent waiters return ctx.error safely
…ignature

Replace 15 overload declarations (3 per method × 5 methods) and 4
helper functions (isAbortSignalLike, isRestRequestOptions,
normalizeGetRequestOptions, normalizeSignalOrOptions) with a single
RestRequestOptions signature per method.

At 0.x, backward-compatible overloads aren't worth the duck-typing
heuristic complexity. Callers migrate:
  get(url, params, signal) → get(url, { params, signal })
  post(url, data, signal)  → post(url, data, { signal })
  delete(url, signal)      → delete(url, { signal })
Signed-off-by: G S <grigoriis.dev@gmail.com>
Major:
- Remove .agents/ from PR (unrelated tooling, now gitignored)
- Fix vite.config.ts resolve.dedupe: @hai3/* -> @cyberfabric/*
- Re-export auth contract types (AuthProvider, AuthSession, etc.)
  from @cyberfabric/react for consumer convenience
- Guard Object.assign in createHAI3.ts: fail fast if plugin app
  extension key conflicts with existing app property
- Refactor AuthSession to discriminated union: BearerAuthSession
  (token required), CookieAuthSession (csrfToken), CustomAuthSession
  (open shape). Enables proper narrowing by session.kind

Medium:
- Remove unnecessary RestResponseContext type assertions in RestProtocol
- Fix incorrect resolves.not.toThrow() matcher in auth test
- Update API.md: remove deprecated positional signal example
@voidtopixel voidtopixel marked this pull request as ready for review April 6, 2026 09:03
@gs-layer
Copy link
Copy Markdown
Owner

gs-layer commented Apr 7, 2026

Code Review: PR #1 — Refactor authentication structure

Overall Assessment

This PR proposes a thoughtful restructuring of the auth architecture with a two-layer contract and React hooks. Several ideas here are genuinely valuable — identity type inference, lifecycle hooks with app context, and React hooks for common auth operations. That said, there are architectural and compatibility concerns worth discussing before merging.


What works well

1. React hooks out of the box useLogin(), useLogout(), useGetIdentity(), useAuthProvider() provide idiomatic React DX. Writing const login = useLogin() is significantly cleaner than app.auth?.login?.(...). This is the right direction for the React layer.

2. Identity type inference via module augmentation The AppRuntimeExtensions + typeof authProvider pattern gives autocomplete for custom identity fields without requiring generics at call-sites:

const { identity } = useGetIdentity();
identity.role    // ← autocomplete works
identity.orgId   // ← no generic needed

This is an elegant solution to a real DX pain point.

3. Custom method pass-through frontxAuthProvider() with the TExtra generic allows adding methods like requestPasswordReset() or verifyMfa() that remain fully typed through useAuthProvider(). This is not supported in the current implementation and would be a welcome addition.

4. Provider lifecycle with app context onAppInit(app) / onAppDestroy(app) gives the provider a reference to HAI3App, which is more capable than destroy() without arguments. The provider can access registries, store, and other app-level resources during initialization and cleanup.

5. Clean file decomposition Splitting into auth-rest-plugin.ts, transport-utils.ts, provider.ts, plugin.ts — one responsibility per file — improves readability compared to the current single auth.ts (~290 LOC).


Architectural concerns

1. Two-layer contract introduces ambiguity in required methods

The base AuthProvider requires login() + logout(), but not getSession() or checkAuth(). FrontxAuthProvider extends the base and adds getSession() (required) + checkAuth() (required).

This creates two different sets of required methods:

Base:   login (required), logout (required), checkAuth (optional)
FrontX: login (required), logout (required), checkAuth (required), getSession (required)

A developer sees AuthProvider in types.ts and assumes they need to implement login + logout. They later discover that REST transport requires FrontxAuthProvider with getSession(). There is no single answer to "which contract do I implement?"

The current PR cyberfabric#260 uses a single contract: 3 required methods (getSession, checkAuth, logout), everything else optional. One question, one answer.

2. login() as required in the base contract is overly restrictive

Cookie-session providers often lack a programmatic login — the server sets a cookie via redirect. SAML/OIDC providers work similarly: login means redirecting to an IdP, not calling a method. Forcing login as required produces boilerplate stubs:

login: async () => ({ type: 'none' }), // no-op for cookie sessions

In PR cyberfabric#260, login is optional — if the provider does not support programmatic login, the method is simply not declared.

3. Removing @cyberfabric/auth as a standalone package

The PR deletes packages/auth/ and moves types into packages/framework/. This does solve the pnpm virtual store problem (which was a real issue), but introduces a new one:

  • Auth types become coupled to @cyberfabric/framework (L2). The contract cannot be used without the full framework dependency.
  • Backend utilities, test mocks, and CLI tooling would all need to depend on framework just for types.
  • FrontX guidelines define L1 (SDK, zero deps) and L2 (framework, depends on SDK). Pure auth types are an L1 concern. Moving them to L2 breaks the established layering.

The pnpm issue has a simpler resolution: using link: instead of file: in consumer apps combined with resolve.dedupe (already implemented in PR cyberfabric#260).

4. Three levels of type augmentation add complexity

The PR introduces a chain:

User app → AppRuntimeExtensions (@cyberfabric/react)
         → __CyberfabricReactAppRuntimeExtensions (global)
         → AppRuntimeExtensions (@cyberfabric/framework)
         → ResolvedAuthProvider = ResolveKey<AppRuntimeExtensions, 'authProvider', AuthProvider>

Three interface merges, a conditional type, and a global declaration. ResolveKey<T, K, Fallback> is a custom conditional type for fallback resolution. This works, but debugging TypeScript errors through this chain is challenging. PR cyberfabric#260 uses a single HAI3AppRuntimeExtensions interface with straightforward module augmentation.

5. app.getAuthProvider() vs app.auth

The PR replaces app.auth.login() with app.getAuthProvider()?.login():

  • More verbose: app.getAuthProvider()?.login() vs app.auth?.login()
  • Less discoverable: app.auth appears immediately in autocomplete; app.getAuthProvider requires knowing it exists
  • In headless contexts (tests, scripts, Node.js), the getter is less ergonomic

The React hooks compensate for this in UI code, but headless usage is a common scenario.

6. Transport binding is no longer pluggable

PR cyberfabric#260 provides a transport option in the auth() config, allowing replacement of the default hai3ApiTransport() binding for non-REST scenarios (WebSocket auth, gRPC, custom request signing).

This PR removes that extension point — transport is built into frontxAuthProvider(). Custom transport requires skipping frontxAuthProvider() entirely and reimplementing the full binding manually.


Compatibility with FrontX

1. Layering FrontX architecture: L1 (SDK, zero deps) → L2 (framework) → L3 (React). Auth types are an L1 concern. This PR places them in L2, which conflicts with the GUIDELINES.md routing where packages/auth is listed under SDK Layer.

2. Breaking changes

  • app.auth.*  app.getAuthProvider().* — breaks all existing consumers
  • login() becomes required — breaks cookie-session providers
  • @cyberfabric/auth removal — breaks import paths
  • hai3ApiTransport() removal — breaks custom transport configurations

Breaking changes are acceptable at 0.x, but the scope here is significant.

3. Scope This PR combines several independent concerns:

  • Contract restructuring (structural change)
  • Package removal (dependency change)
  • React hooks (feature addition)
  • Module augmentation pattern (DX improvement)
  • Full test suite rewrite

Each of these would benefit from being a separately reviewable PR. Combining them makes review harder and complicates rollback if issues arise.


My recommendation for further work

Several ideas from this PR would be valuable as incremental follow-ups to the current architecture:

Idea Priority Suggested scope
React hooks (useLogin, useLogout, useGetIdentity, useAuthProvider) High Separate PR to @cyberfabric/react
Identity generics via module augmentation Medium Extension of AuthIdentity in @cyberfabric/auth
onAppInit(app) / onAppDestroy(app) lifecycle Medium Replace destroy() in auth plugin
Custom provider method pass-through Lower After base contract stabilizes

Concerns that suggest keeping the current approach:

  • Removing the @cyberfabric/auth package (breaks L1/L2 layering)
  • Two-layer base/frontx contract (ambiguity in required methods)
  • login() as required in the base (not universal across auth mechanisms)
  • Removing pluggable transport (reduces flexibility)

@gs-layer
Copy link
Copy Markdown
Owner

gs-layer commented Apr 7, 2026

Verdict for RBAC

Criterion PR cyberfabric#260 PR #1
Standalone RBAC types (L1) ✅ @cyberfabric/auth has zero deps ❌ Types in L2 framework
Pluggable transport for RBAC middleware ✅ transport option ❌ Removed
Type-safe identity for RBAC attributes ⚠️ attributes bag, not generic ✅ Generic TIdentity
App lifecycle for permission preloading ⚠️ destroy() only ✅ onAppInit(app)
React hooks for authorization ❌ Not yet ⚠️ useAuthProvider() exists, no useCanAccess()
Custom RBAC methods (typed) ❌ Not supported ✅ TExtra pass-through
RBAC-aware transport enforcement ✅ Via custom transport binder ⚠️ Manual plugin registration

PR cyberfabric#260 is better positioned for RBAC contract integration because:

  1. Standalone types — a future @cyberfabric/rbac package can import AccessQuery, AccessDecision, AuthPermissions from @cyberfabric/auth without pulling in the framework. This is important for server-side authorization checks, shared RBAC utilities, and test infrastructure.

  2. Pluggable transport — RBAC enforcement at the transport level (e.g., calling canAccess() before every API request) is a natural extension of the transport option. In PR Refactor authentication structure by removing 'auth' package #1, this requires a separate manually-managed plugin.

  3. Single contract — one set of required methods means an RBAC decorator/wrapper around AuthProvider has a clear, unambiguous interface to implement. The two-layer contract in PR Refactor authentication structure by removing 'auth' package #1 creates ambiguity about which layer to extend for RBAC.

Valuable ideas from PR #1 for future RBAC work:

  • Generic identity types — would make RBAC attributes type-safe without the attributes bag
  • onAppInit(app) — useful for preloading permission caches at startup
  • React hooks — useCanAccess() would be the idiomatic way to check permissions in components
  • Custom method pass-through — preloadPermissions(), getRolesForResource() etc.

These can be added incrementally to the PR cyberfabric#260 architecture without the breaking changes PR #1 introduces.

@gs-layer gs-layer force-pushed the feat/auth-provider branch 2 times, most recently from 7d60d67 to 448b42a Compare April 23, 2026 09:31
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.

3 participants