Files
WRNexusJSDoc/app/pages/packages/helpers.wrn
T

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.0</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.0</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.0</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 &#123; Context &#125; from &quot;@wrnexus/core&quot;;
import &#123; redirectToLogin &#125; from &quot;@wrnexus/helpers&quot;;
export const GET = async (ctx: Context) =&gt; &#123;
if (await hasValidSession(ctx)) &#123;
return new Response(null, &#123; status: 204 &#125;);
&#125;
return redirectToLogin(ctx, &quot;/login&quot;, &#123;
allowedHosts: [&quot;admin.localhost:3000&quot;, &quot;reports.localhost:3000&quot;],
&#125;);
&#125;;</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 &#123; Context &#125; from &quot;@wrnexus/core&quot;;
import &#123; getOriginalRequestOrigin, redirectToLogin &#125; from &quot;@wrnexus/helpers&quot;;
export const GET = async (ctx: Context) =&gt; &#123;
const allowedHosts = (host: string) =&gt; host === &quot;example.test&quot; || host.endsWith(&quot;.example.test&quot;);
console.info(&quot;Authentication requested by&quot;, getOriginalRequestOrigin(ctx, &#123; allowedHosts &#125;));
return redirectToLogin(ctx, &quot;https://auth.example.test/login&quot;, &#123;
allowedHosts,
returnToParam: &quot;continue&quot;,
status: 303,
&#125;);
&#125;;</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 &#123; Context &#125; 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&lt;Record&lt;string, string&gt;&gt;;
/** Shared DNS suffix for configured workspace apps (for example `staging.example.com`). */
declare function workspaceRootDomain(): string;
interface RetryOptions &#123;
attempts?: number;
minDelayMs?: number;
maxDelayMs?: number;
factor?: number;
jitter?: number;
signal?: AbortSignal;
retryIf?: (error: unknown, attempt: number) =&gt; boolean | Promise&lt;boolean&gt;;
onRetry?: (error: unknown, attempt: number, delayMs: number) =&gt; void | Promise&lt;void&gt;;
&#125;
declare function backoffDelay(attempt: number, options?: Pick&lt;RetryOptions, &quot;minDelayMs&quot; | &quot;maxDelayMs&quot; | &quot;factor&quot; | &quot;jitter&quot;&gt;): number;
declare function sleep(ms: number, signal?: AbortSignal): Promise&lt;void&gt;;
declare function retry&lt;T&gt;(operation: (attempt: number, signal?: AbortSignal) =&gt; Promise&lt;T&gt;, options?: RetryOptions): Promise&lt;T&gt;;
declare function withTimeout&lt;T&gt;(promise: Promise&lt;T&gt;, timeoutMs: number, message?: string, signal?: AbortSignal): Promise&lt;T&gt;;
declare function stableStringify(value: unknown): string;
declare function safeJsonParse&lt;T&gt;(value: string, fallback: T): T;
declare function clamp(value: number, min: number, max: number): number;
declare function once&lt;T extends (...args: any[]) =&gt; any&gt;(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&lt;Context, &quot;req&quot; | &quot;url&quot;&gt;;
type AllowedHosts = readonly string[] | ReadonlySet&lt;string&gt; | ((host: string, ctx: RequestContext) =&gt; boolean);
interface OriginalRequestOptions &#123;
/**
* Hosts that the application permits as redirect destinations. This is
* required when a proxy supplied X-Forwarded-Host is present.
*/
allowedHosts?: AllowedHosts;
&#125;
interface LoginRedirectOptions extends OriginalRequestOptions &#123;
/** Query parameter that receives the original absolute URL. */
returnToParam?: string;
/** Browser redirect status. Defaults to 302. */
status?: 301 | 302 | 303 | 307 | 308;
&#125;
/** 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 &#123; 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 &#125;;
</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 &#123; Context &#125; from &quot;@wrnexus/core&quot;;
import &#123; redirectToLogin &#125; from &quot;@wrnexus/helpers&quot;;
export const GET = async (ctx: Context) =&gt; &#123;
if (await hasValidSession(ctx)) &#123;
return new Response(null, &#123; status: 204 &#125;);
&#125;
return redirectToLogin(ctx, &quot;/login&quot;, &#123;
allowedHosts: [&quot;admin.localhost:3000&quot;, &quot;reports.localhost:3000&quot;],
&#125;);
&#125;;</code></pre></article><article class="example-card"><h3>Support dynamic tenant domains</h3><pre data-language="ts"><code>import type &#123; Context &#125; from &quot;@wrnexus/core&quot;;
import &#123; getOriginalRequestOrigin, redirectToLogin &#125; from &quot;@wrnexus/helpers&quot;;
export const GET = async (ctx: Context) =&gt; &#123;
const allowedHosts = (host: string) =&gt; host === &quot;example.test&quot; || host.endsWith(&quot;.example.test&quot;);
console.info(&quot;Authentication requested by&quot;, getOriginalRequestOrigin(ctx, &#123; allowedHosts &#125;));
return redirectToLogin(ctx, &quot;https://auth.example.test/login&quot;, &#123;
allowedHosts,
returnToParam: &quot;continue&quot;,
status: 303,
&#125;);
&#125;;</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.0</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>
}
}