/** * OAuth Session Validation Wrapper * * Wraps a Harper Resource **class** so HTTP methods run OAuth session * validation (and automatic token refresh) before the wrapped method * executes. The returned value is a subclass of the input class: Harper * registers it via `resources.set(...)` and invokes it through the * standard `static getResource` lifecycle. Per-request instances inherit * `getContext()` from the user's base `Resource`, so the validation runs * against the real request context with no extra plumbing. */ import type { Request, Logger, ProviderRegistry } from '../types.ts'; export interface OAuthValidationOptions { /** OAuth provider registry from plugin initialization */ providers: ProviderRegistry; /** Logger instance for debugging */ logger?: Logger; /** Whether to require OAuth authentication (401 if not present) */ requireAuth?: boolean; /** * Custom error handler for validation failures. * * **Only invoked when `requireAuth` is `true`**. When `requireAuth` * is `false` the wrapper passes through silently on any validation * failure (cleaning up stale session data as a side effect) — this * callback is not called. If you need audit logging on a mixed-auth * resource, set `requireAuth: true` on that resource, or log from * your own `logger` (which IS always invoked). * * **Session state visibility depending on the failure path:** * - `!hasOAuth` — `request.session.oauth` is already `undefined` * (there never was any). * - `!providerName` / `!providerData` (stale-provider paths) — the * callback is invoked BEFORE session cleanup, so * `request.session.oauth` and `.oauthUser` are readable. * - `!validation.valid` (expired token with no refresh token) — * `validateAndRefreshSession` has ALREADY called * `clearOAuthSession` internally before the callback runs. On a * production Harper session this calls `session.delete(session.id)` * (DB record destroyed; in-memory fields untouched). On a session * without a `delete()` method it falls back to in-memory deletion * of `.oauth` / `.oauthUser`. The callback is still invoked, but * the session state it observes depends on which path ran. */ onValidationError?: (request: Request, error: string) => any; } /** * Wrap a Harper `Resource` class so each HTTP method runs OAuth session * validation before the user-defined method executes. * * @example * ```typescript * // In your application component: * import { Resource } from 'harper'; * import { withOAuthValidation } from '@harperfast/oauth'; * * export function handleApplication(scope) { * const oauthPlugin = scope.parent.resources.get('oauth'); * * class MyResource extends Resource { * static loadAsInstance = false; * async get(target) { * const request = this.getContext(); * return { user: request.session.oauthUser }; * } * } * * const Protected = withOAuthValidation(MyResource, { * providers: oauthPlugin.providers, * requireAuth: true, * logger: scope.logger, * }); * * // Register the wrapped class — Harper handles instantiation per request * scope.resources.set('protected', Protected); * } * ``` * * Notes: * - The wrapper returns a **subclass** of `ResourceClass`. All static * properties (including `loadAsInstance`) and static methods * (including `getResource`) are inherited, so Harper's registration * and dispatch lifecycle works unchanged. * - Validation is installed at BOTH static and instance levels so * either Harper v5 dispatch pattern is intercepted: * - User defines `async get(target)` (instance method): Harper's * Resource base static `get` creates a Wrapped instance and * invokes the instance method, which runs validation first. * - User defines `static async get(target)` (v5-recommended pattern * per Harper's migration guide): Harper calls `Wrapped.get` * directly at the static level. Our static override catches this * and runs validation before delegating. * - Only the verbs the user class actually implements (own static OR * own instance method) get overridden. Unimplemented verbs are * explicitly shadowed with `undefined` on the wrapper to blot out * the Resource base's inherited transactional static, so Harper's * native "405 Method Not Allowed" response (via `missingMethod` at * server/REST.ts:117) is preserved — rather than our wrapper running * validation on a verb the user never meant to expose. * - Validation is de-duplicated across the static→instance call chain * via a per-request `WeakSet`. Both the request object AND * `request.getContext?.()` are stamped, because Harper's transactional * wrapper normalizes the request during dispatch — the object the * instance method observes via `this.getContext()` may be a different * reference than the one the static override saw. `validateAndRefresh * Session` can hit the network for token refresh; running it twice per * request would waste a round-trip. * * Session-cleanup semantics (intentional divergence — important for * integrators using `requireAuth: false`): * - Stale-provider paths (no provider name on session, or provider * not in registry): the wrapper clears only the in-memory `oauth` * and `oauthUser` fields via a local helper. The session record * itself survives — "provider not configured" may be a recoverable * config issue. * - Expired-token path (`validateAndRefreshSession` returns * `{valid: false}`): `validateAndRefreshSession` internally calls * `clearOAuthSession`, which on a Harper production session * invokes `session.delete(session.id)` — the DB record is destroyed. * This is terminal: the user is logged out, not just detached from * OAuth. `requireAuth: false` resources still receive the * passthrough call, but they observe a session that is about to * stop existing on the next request. */ export declare function withOAuthValidation any>(ResourceClass: T, options: OAuthValidationOptions): T; /** * Helper to get OAuth providers from the OAuth plugin * Call this from your application to access the provider registry * * @example * ```typescript * import { getOAuthProviders } from '@harperfast/oauth'; * * export function handleApplication(scope) { * const providers = getOAuthProviders(scope); * // Use providers with withOAuthValidation * } * ``` */ export declare function getOAuthProviders(scope: any): ProviderRegistry | null;