163 lines
12 KiB
Plaintext
163 lines
12 KiB
Plaintext
page wrnexushelpers {
|
|
seo {
|
|
title = "@wrnexus/helpers"
|
|
description = "Safe Context URL helpers and forward-auth login redirects."
|
|
}
|
|
|
|
view {
|
|
<div class="docs-shell">
|
|
<a href="#main" class="skip-link">Skip to content</a>
|
|
<header class="topbar">
|
|
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
|
<nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
|
<div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v0.8.5</a><button data-wire-theme-toggle class="theme-button" aria-label="Toggle color theme" title="Toggle color theme">◐</button></div>
|
|
</header>
|
|
<div class="mobile-doc-nav"><details><summary>Browse documentation</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/guides/project-structure">Guides</a><a href="/examples">Examples</a><a href="/search">Search</a></nav></details></div>
|
|
<main class="portal-main docs-layout">
|
|
<article id="main" class="documentation prose standalone package-document"><nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span>/</span><a href="/packages">Packages</a><span>/</span><span aria-current="page">@wrnexus/helpers</span></nav><section class="doc-intro"><span class="eyebrow">Tooling · Package reference</span><h1>@wrnexus/helpers</h1><p>Safe Context URL helpers and forward-auth login redirects.</p><div class="doc-meta"><span>v0.8.5</span><span>Private registry</span><span>Tooling</span></div><section id="access" class="access-callout"><h2>Install the package</h2><p>After WorkRoot approves private registry access, install the release-aligned package:</p><pre><code>bun add @wrnexus/helpers@0.8.5</code><button type="button" class="copy-button" aria-label="Copy installation command">Copy</button></pre><p><a href="/access">Request preview access</a>. Never put registry tokens in source control.</p></section></section><section id="guide"><p>Safe convenience helpers for common WRNexusJS application flows. The package uses standard <code>Context</code>, <code>URL</code>, and <code>Response</code> values and has no runtime dependency beyond <code>@wrnexus/core</code>.</p>
|
|
<pre data-language="bash"><code>bun add @wrnexus/helpers</code></pre>
|
|
<p>The package is private, so the machine must be authenticated to the <code>wrnexus</code> npm organization.</p>
|
|
<h3 id="usage">Usage</h3>
|
|
<h4 id="redirect-an-unauthenticated-forward-auth-request">Redirect an unauthenticated forward-auth request</h4>
|
|
<p>The gateway calls an SSO verifier on a different URL from the original application. These helpers reconstruct the original URL from the gateway headers and safely place it in the login redirect:</p>
|
|
<pre data-language="ts"><code>import type { Context } from "@wrnexus/core";
|
|
import { redirectToLogin } from "@wrnexus/helpers";
|
|
|
|
export const GET = async (ctx: Context) => {
|
|
if (await hasValidSession(ctx)) {
|
|
return new Response(null, { status: 204 });
|
|
}
|
|
|
|
return redirectToLogin(ctx, "/login", {
|
|
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
|
|
});
|
|
};</code></pre>
|
|
<p>This creates a response such as:</p>
|
|
<pre data-language="text"><code>Location: http://sso.localhost:3000/login?returnTo=http%3A%2F%2Fadmin.localhost%3A3000%2F</code></pre>
|
|
<p>Always list the application hosts that are valid redirect destinations. Forwarded host headers are rejected when <code>allowedHosts</code> is absent or does not match, preventing an open redirect.</p>
|
|
<p>The SSO hostname is the login destination, not an <code>allowedHosts</code> entry. For example, when protecting <code>admin.localhost:3000</code>, keep <code>admin.localhost:3000</code> in the allowlist even though the verifier runs at <code>sso.localhost:3000</code>. WRNexus preserves both hosts across a nested gateway request.</p>
|
|
<h4 id="support-dynamic-tenant-domains">Support dynamic tenant domains</h4>
|
|
<pre data-language="ts"><code>import type { Context } from "@wrnexus/core";
|
|
import { getOriginalRequestOrigin, redirectToLogin } from "@wrnexus/helpers";
|
|
|
|
export const GET = async (ctx: Context) => {
|
|
const allowedHosts = (host: string) => host === "example.test" || host.endsWith(".example.test");
|
|
|
|
console.info("Authentication requested by", getOriginalRequestOrigin(ctx, { allowedHosts }));
|
|
return redirectToLogin(ctx, "https://auth.example.test/login", {
|
|
allowedHosts,
|
|
returnToParam: "continue",
|
|
status: 303,
|
|
});
|
|
};</code></pre>
|
|
<h3 id="api">API</h3>
|
|
<ul>
|
|
<li><code>getOriginalRequestUrl(ctx, options): URL</code> — reconstruct the gateway URL.</li>
|
|
<li><code>getOriginalRequestOrigin(ctx, options): string</code> — return only its origin.</li>
|
|
<li><code>getOriginalRequestPath(ctx): string</code> — return its path and query string.</li>
|
|
<li><code>getOriginalRequestMethod(ctx): string</code> — return its HTTP method.</li>
|
|
<li><code>redirectToLogin(ctx, loginUrl, options): Response</code> — create a login redirect with an</li>
|
|
<p>encoded <code>returnTo</code> parameter.</p>
|
|
</ul>
|
|
<p>For direct requests without gateway headers, URL helpers use <code>ctx.url</code>.</p></section><section id="api" class="api"><h2>Complete TypeScript API</h2><p>Generated from the exact installed package declarations.</p><pre data-language="typescript"><code>import { Context } from '@wrnexus/core';
|
|
|
|
declare function appOrigin(appName: string): string;
|
|
declare function appUrl(appName: string, path?: string): string;
|
|
declare function currentAppName(): string | undefined;
|
|
declare function currentAppOrigin(): string | undefined;
|
|
declare function workspaceAppOrigins(): Readonly<Record<string, string>>;
|
|
/** Shared DNS suffix for configured workspace apps (for example `staging.example.com`). */
|
|
declare function workspaceRootDomain(): string;
|
|
|
|
interface RetryOptions {
|
|
attempts?: number;
|
|
minDelayMs?: number;
|
|
maxDelayMs?: number;
|
|
factor?: number;
|
|
jitter?: number;
|
|
signal?: AbortSignal;
|
|
retryIf?: (error: unknown, attempt: number) => boolean | Promise<boolean>;
|
|
onRetry?: (error: unknown, attempt: number, delayMs: number) => void | Promise<void>;
|
|
}
|
|
declare function backoffDelay(attempt: number, options?: Pick<RetryOptions, "minDelayMs" | "maxDelayMs" | "factor" | "jitter">): number;
|
|
declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
declare function retry<T>(operation: (attempt: number, signal?: AbortSignal) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message?: string, signal?: AbortSignal): Promise<T>;
|
|
declare function stableStringify(value: unknown): string;
|
|
declare function safeJsonParse<T>(value: string, fallback: T): T;
|
|
declare function clamp(value: number, min: number, max: number): number;
|
|
declare function once<T extends (...args: any[]) => any>(fn: T): T;
|
|
|
|
/**
|
|
* @wrnexus/helpers — safe conveniences for common WRNexusJS application flows.
|
|
*
|
|
* Helpers stay small and composable. They accept the standard WRNexusJS Context
|
|
* and return web-platform values such as URL and Response.
|
|
*/
|
|
|
|
type RequestContext = Pick<Context, "req" | "url">;
|
|
type AllowedHosts = readonly string[] | ReadonlySet<string> | ((host: string, ctx: RequestContext) => boolean);
|
|
interface OriginalRequestOptions {
|
|
/**
|
|
* Hosts that the application permits as redirect destinations. This is
|
|
* required when a proxy supplied X-Forwarded-Host is present.
|
|
*/
|
|
allowedHosts?: AllowedHosts;
|
|
}
|
|
interface LoginRedirectOptions extends OriginalRequestOptions {
|
|
/** Query parameter that receives the original absolute URL. */
|
|
returnToParam?: string;
|
|
/** Browser redirect status. Defaults to 302. */
|
|
status?: 301 | 302 | 303 | 307 | 308;
|
|
}
|
|
/** Get the original path and query string seen by the gateway. */
|
|
declare function getOriginalRequestPath(ctx: RequestContext): string;
|
|
/** Get the original HTTP method seen by the gateway. */
|
|
declare function getOriginalRequestMethod(ctx: RequestContext): string;
|
|
/**
|
|
* Reconstruct the absolute URL that reached the gateway.
|
|
*
|
|
* Forwarded hosts are never trusted implicitly: pass allowedHosts when this is
|
|
* used behind the WRNexusJS gateway. Direct requests fall back to ctx.url.
|
|
*/
|
|
declare function getOriginalRequestUrl(ctx: RequestContext, options?: OriginalRequestOptions): URL;
|
|
/** Get the original request origin, for example http://admin.localhost:3000. */
|
|
declare function getOriginalRequestOrigin(ctx: RequestContext, options?: OriginalRequestOptions): string;
|
|
/**
|
|
* Redirect to a login page with the original absolute URL encoded as returnTo.
|
|
* Relative login URLs resolve against the current app (normally the SSO app).
|
|
*/
|
|
declare function redirectToLogin(ctx: RequestContext, loginUrl: string | URL, options?: LoginRedirectOptions): Response;
|
|
|
|
export { type AllowedHosts, type LoginRedirectOptions, type OriginalRequestOptions, type RequestContext, type RetryOptions, appOrigin, appUrl, backoffDelay, clamp, currentAppName, currentAppOrigin, getOriginalRequestMethod, getOriginalRequestOrigin, getOriginalRequestPath, getOriginalRequestUrl, once, redirectToLogin, retry, safeJsonParse, sleep, stableStringify, withTimeout, workspaceAppOrigins, workspaceRootDomain };
|
|
</code></pre></section><section id="examples" class="examples"><h2>Examples</h2><p>Copy-ready examples from the installed package documentation.</p><div class="example-grid"><article class="example-card"><h3>Redirect an unauthenticated forward-auth request</h3><pre data-language="ts"><code>import type { Context } from "@wrnexus/core";
|
|
import { redirectToLogin } from "@wrnexus/helpers";
|
|
|
|
export const GET = async (ctx: Context) => {
|
|
if (await hasValidSession(ctx)) {
|
|
return new Response(null, { status: 204 });
|
|
}
|
|
|
|
return redirectToLogin(ctx, "/login", {
|
|
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
|
|
});
|
|
};</code></pre></article><article class="example-card"><h3>Support dynamic tenant domains</h3><pre data-language="ts"><code>import type { Context } from "@wrnexus/core";
|
|
import { getOriginalRequestOrigin, redirectToLogin } from "@wrnexus/helpers";
|
|
|
|
export const GET = async (ctx: Context) => {
|
|
const allowedHosts = (host: string) => host === "example.test" || host.endsWith(".example.test");
|
|
|
|
console.info("Authentication requested by", getOriginalRequestOrigin(ctx, { allowedHosts }));
|
|
return redirectToLogin(ctx, "https://auth.example.test/login", {
|
|
allowedHosts,
|
|
returnToParam: "continue",
|
|
status: 303,
|
|
});
|
|
};</code></pre></article></div></section></article>
|
|
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-4" href="#redirect-an-unauthenticated-forward-auth-request">Redirect an unauthenticated forward-auth request</a><a class="toc-level-4" href="#support-dynamic-tenant-domains">Support dynamic tenant domains</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
|
</main>
|
|
<footer><div class="footer-brand"><span class="footer-mark" aria-hidden="true">W</span><p><strong>WRNexusJS 0.8.5</strong><span>Complete API documentation generated from installed package declarations.</span></p></div><nav aria-label="Footer"><a href="/packages">All packages</a><a href="/getting-started">Get started</a><a href="/security">Security</a><a href="/support">Support</a><a href="/llms.txt">AI guide</a></nav><p class="footer-meta">Private Developer Preview · Bun-native</p></footer>
|
|
</div>
|
|
}
|
|
}
|