@relyt/auth 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/errors.js ADDED
@@ -0,0 +1,42 @@
1
+ const MESSAGES = {
2
+ unauthenticated: "not authenticated",
3
+ unavailable: "auth service unavailable",
4
+ network: "network error",
5
+ bad_response: "unexpected response from auth service",
6
+ invalid_token: "invalid token",
7
+ invalid_claims: "invalid token claims",
8
+ jwks_unavailable: "signing keys unavailable",
9
+ config: "invalid configuration",
10
+ invalid_handoff_code: "invalid or expired sign-in code",
11
+ invalid_origin: "origin not trusted by auth service",
12
+ rate_limited: "too many requests",
13
+ no_pending_sign_in: "no pending sign-in in this browser session",
14
+ missing_code: "sign-in code missing from callback URL",
15
+ oauth_error: "social sign-in failed",
16
+ };
17
+ const RETRYABLE = new Set(["unavailable", "network", "jwks_unavailable"]);
18
+ export class RelytAuthError extends Error {
19
+ code;
20
+ retryable;
21
+ status;
22
+ /** `config` only: name of the offending option (option names are not secret). */
23
+ option;
24
+ /** `rate_limited` only: integer seconds from the `retry-after` response header (when present and valid). */
25
+ retryAfterS;
26
+ /** `oauth_error` only: the `error` value from the callback URL, filtered by `^[a-z0-9_]{1,64}$`, otherwise `"unknown"`. */
27
+ oauthError;
28
+ constructor(code, o = {}) {
29
+ super(MESSAGES[code]);
30
+ this.name = "RelytAuthError";
31
+ this.code = code;
32
+ this.retryable = o.retryable ?? RETRYABLE.has(code);
33
+ if (o.status !== undefined)
34
+ this.status = o.status;
35
+ if (o.option !== undefined)
36
+ this.option = o.option;
37
+ if (o.retryAfterS !== undefined)
38
+ this.retryAfterS = o.retryAfterS;
39
+ if (o.oauthError !== undefined)
40
+ this.oauthError = o.oauthError;
41
+ }
42
+ }
@@ -0,0 +1,5 @@
1
+ export { RelytAuthError, type RelytAuthErrorCode, type AuthEvent } from "./errors.js";
2
+ export { CLAIM_KEYS, REGISTERED_CLAIM_KEYS, type RelytClaims } from "./claims.js";
3
+ export { createVerifier, verifyToken, type VerifierOptions } from "./verify.js";
4
+ export { createAuthClient, type AuthClient, type AuthClientOptions } from "./client.js";
5
+ export { startSocialSignIn, createSocialStart, completeSocialSignIn, exchangeHandoffCode, SOCIAL_PKCE_STORAGE_KEY, type StartSocialSignInOptions, type SocialStart, type CompleteSocialSignInOptions, type ExchangeHandoffCodeOptions, type SocialResponseMode, } from "./social.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // @relyt/auth main entry (Web APIs only: works in browsers, Node and the Next.js edge runtime).
2
+ export { RelytAuthError } from "./errors.js";
3
+ export { CLAIM_KEYS, REGISTERED_CLAIM_KEYS } from "./claims.js";
4
+ export { createVerifier, verifyToken } from "./verify.js";
5
+ export { createAuthClient } from "./client.js";
6
+ export { startSocialSignIn, createSocialStart, completeSocialSignIn, exchangeHandoffCode, SOCIAL_PKCE_STORAGE_KEY, } from "./social.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The issuer must be exactly one origin (`new URL(s).origin === s`): no trailing slash, no path, and nothing that URL
3
+ * normalization would rewrite (case, default port), because the token's `iss` is compared to it verbatim.
4
+ */
5
+ export declare function normalizeIssuer(s: unknown): string;
6
+ /** jwksUrl may have any path, but its origin follows the same rule as the issuer. Returns the normalized href (used as cache key). */
7
+ export declare function normalizeJwksUrl(s: unknown): string;
package/dist/issuer.js ADDED
@@ -0,0 +1,37 @@
1
+ import { RelytAuthError } from "./errors.js";
2
+ /** Plain http is only accepted for exact loopback host names (local development); everything else must be https. */
3
+ const LOOPBACK = new Set(["localhost", "127.0.0.1", "[::1]"]);
4
+ function checkOrigin(u, option) {
5
+ if (u.protocol === "https:")
6
+ return;
7
+ if (u.protocol === "http:" && LOOPBACK.has(u.hostname))
8
+ return;
9
+ throw new RelytAuthError("config", { option });
10
+ }
11
+ function parse(s, option) {
12
+ if (typeof s !== "string" || s === "")
13
+ throw new RelytAuthError("config", { option });
14
+ try {
15
+ return new URL(s);
16
+ }
17
+ catch {
18
+ throw new RelytAuthError("config", { option });
19
+ }
20
+ }
21
+ /**
22
+ * The issuer must be exactly one origin (`new URL(s).origin === s`): no trailing slash, no path, and nothing that URL
23
+ * normalization would rewrite (case, default port), because the token's `iss` is compared to it verbatim.
24
+ */
25
+ export function normalizeIssuer(s) {
26
+ const u = parse(s, "issuer");
27
+ if (u.origin !== s)
28
+ throw new RelytAuthError("config", { option: "issuer" });
29
+ checkOrigin(u, "issuer");
30
+ return u.origin;
31
+ }
32
+ /** jwksUrl may have any path, but its origin follows the same rule as the issuer. Returns the normalized href (used as cache key). */
33
+ export function normalizeJwksUrl(s) {
34
+ const u = parse(s, "jwksUrl");
35
+ checkOrigin(u, "jwksUrl");
36
+ return u.href;
37
+ }
@@ -0,0 +1,2 @@
1
+ /** Three non-empty segments and a base64url UTF-8 JSON object payload ⇒ that object; otherwise null. Never throws. */
2
+ export declare function decodePayloadUnverified(token: string): Record<string, unknown> | null;
@@ -0,0 +1,21 @@
1
+ // Reads a JWT payload without verifying it (shared by client.ts and next.ts).
2
+ // Only for local hints such as lifetime / Max-Age; trust decisions must go through verifyToken. Web APIs only (edge / browser).
3
+ /** Three non-empty segments and a base64url UTF-8 JSON object payload ⇒ that object; otherwise null. Never throws. */
4
+ export function decodePayloadUnverified(token) {
5
+ if (typeof token !== "string")
6
+ return null;
7
+ const parts = token.split(".");
8
+ if (parts.length !== 3 || parts.some((p) => p === ""))
9
+ return null;
10
+ try {
11
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
12
+ const bin = atob(b64 + "=".repeat((4 - (b64.length % 4)) % 4));
13
+ const payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(bin, (c) => c.charCodeAt(0))));
14
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload))
15
+ return null;
16
+ return payload;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
package/dist/next.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { NextResponse, type NextRequest } from "next/server.js";
2
+ import { RelytAuthError } from "./errors.js";
3
+ import { type VerifierOptions } from "./verify.js";
4
+ import type { RelytClaims } from "./claims.js";
5
+ /** Default cookie name shared by the middleware, getClaims and set/clearTokenCookie. */
6
+ export declare const DEFAULT_COOKIE_NAME = "__Host-relyt-at";
7
+ export interface AuthMiddlewareOptions extends VerifierOptions {
8
+ /** Sign-in page; absolute URLs are used as-is, relative paths are resolved against the request URL. */
9
+ signInUrl: string;
10
+ /** Strings match by path-segment prefix; RegExps are tested against the pathname. */
11
+ publicPaths?: Array<string | RegExp>;
12
+ /** Default "/api/". */
13
+ apiPrefix?: string;
14
+ /** Default "__Host-relyt-at". */
15
+ cookieName?: string;
16
+ /** Default true; false ⇒ API paths accept bearer tokens only. */
17
+ cookieOnApi?: boolean;
18
+ }
19
+ /** JWKS unavailable (code "jwks_unavailable"): handlers should answer 503. */
20
+ export declare class RelytAuthUnavailableError extends RelytAuthError {
21
+ constructor();
22
+ }
23
+ /**
24
+ * Normalizes `next`: decode first (failure ⇒ "/"); after decoding, any C0 control character / DEL (URL parsing strips \t\n\r, so "/\t/evil.com" would become "//evil.com"; spaces are allowed since the origin check below still applies),
25
+ * a leading // or /\, any \, no leading /, or a cross-origin result when resolved against a placeholder origin ⇒ "/".
26
+ */
27
+ export declare function safeNext(pathAndSearch: string): string;
28
+ export declare function createAuthMiddleware(o: AuthMiddlewareOptions): (req: NextRequest) => Promise<NextResponse>;
29
+ /**
30
+ * Reads claims in a route handler: takes the token from the bearer header (preferred) or the cookie and verifies it again; never reads x-relyt-*.
31
+ * `cookie` defaults to true; false ⇒ Authorization only (matches the middleware's `cookieOnApi: false`).
32
+ * Missing / invalid / other-tenant token ⇒ null; JWKS unavailable ⇒ throws RelytAuthUnavailableError by default, `onUnavailable: "null"` ⇒ null.
33
+ */
34
+ export declare function getClaims(req: Request, o: VerifierOptions & {
35
+ cookieName?: string;
36
+ }, opts?: {
37
+ onUnavailable?: "throw" | "null";
38
+ cookie?: boolean;
39
+ }): Promise<RelytClaims | null>;
40
+ /** Writes the token cookie: Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=min(TOKEN_TTL_S, exp − now); remaining ≤ 0 ⇒ clears it instead. */
41
+ export declare function setTokenCookie(res: NextResponse, token: string, o?: {
42
+ cookieName?: string;
43
+ }): void;
44
+ /** Clears the token cookie (sign-out must call this server-side: the cookie is HttpOnly, browser JS cannot clear it). */
45
+ export declare function clearTokenCookie(res: NextResponse, o?: {
46
+ cookieName?: string;
47
+ }): void;
package/dist/next.js ADDED
@@ -0,0 +1,205 @@
1
+ // @relyt/auth/next: Next.js middleware entry. Web APIs + next/server only, usable in the edge runtime.
2
+ // Security notes:
3
+ // - The issuer comes only from configuration (options.issuer), never from the request's Host / X-Forwarded-Host; token iss/aud are compared to it verbatim.
4
+ // - Incoming x-relyt-* headers are always stripped, on success or failure; the middleware never forwards claim headers, route handlers re-verify with getClaims.
5
+ // - Tokens and cookie values never reach logs, responses or events.
6
+ import { NextResponse } from "next/server.js";
7
+ import { TOKEN_TTL_S } from "./constants.js";
8
+ import { RelytAuthError } from "./errors.js";
9
+ import { normalizeIssuer } from "./issuer.js";
10
+ import { decodePayloadUnverified } from "./jwt-payload.js";
11
+ import { createVerifier, verifyToken } from "./verify.js";
12
+ /** Default cookie name shared by the middleware, getClaims and set/clearTokenCookie. */
13
+ export const DEFAULT_COOKIE_NAME = "__Host-relyt-at";
14
+ const STRIP_PREFIX = "x-relyt-";
15
+ /** Auth failure responses are never cacheable, so a shared cache cannot replay a 401 / redirect / 503 to a signed-in user. */
16
+ const NO_STORE = "no-store";
17
+ /** JWKS unavailable (code "jwks_unavailable"): handlers should answer 503. */
18
+ export class RelytAuthUnavailableError extends RelytAuthError {
19
+ constructor() {
20
+ super("jwks_unavailable");
21
+ this.name = "RelytAuthUnavailableError";
22
+ }
23
+ }
24
+ function bearer(h) {
25
+ const v = h.get("authorization");
26
+ if (v === null)
27
+ return null;
28
+ const m = /^Bearer[ ]+(\S+)[ ]*$/i.exec(v);
29
+ return m ? m[1] : null;
30
+ }
31
+ /** The cookie value if the name occurs exactly once in the Cookie header; multiple occurrences ⇒ null (prevents shadowing via cookies injected from a sibling subdomain). */
32
+ function cookie(h, name) {
33
+ const raw = h.get("cookie");
34
+ if (raw === null)
35
+ return null;
36
+ const hits = [];
37
+ for (const part of raw.split(";")) {
38
+ const i = part.indexOf("=");
39
+ if (i < 0)
40
+ continue;
41
+ if (part.slice(0, i).trim() === name)
42
+ hits.push(part.slice(i + 1).trim());
43
+ }
44
+ if (hits.length !== 1 || hits[0] === "")
45
+ return null;
46
+ return hits[0];
47
+ }
48
+ /** A present Authorization header is authoritative: non-Bearer / malformed ⇒ no token, with no fallback to the cookie. */
49
+ function readToken(h, name, allowCookie) {
50
+ if (h.has("authorization"))
51
+ return bearer(h);
52
+ return allowCookie ? cookie(h, name) : null;
53
+ }
54
+ function cookieNameOf(v) {
55
+ if (v === undefined)
56
+ return DEFAULT_COOKIE_NAME;
57
+ if (typeof v !== "string" || !/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(v))
58
+ throw new RelytAuthError("config", { option: "cookieName" });
59
+ return v;
60
+ }
61
+ /**
62
+ * Normalizes `next`: decode first (failure ⇒ "/"); after decoding, any C0 control character / DEL (URL parsing strips \t\n\r, so "/\t/evil.com" would become "//evil.com"; spaces are allowed since the origin check below still applies),
63
+ * a leading // or /\, any \, no leading /, or a cross-origin result when resolved against a placeholder origin ⇒ "/".
64
+ */
65
+ export function safeNext(pathAndSearch) {
66
+ let d;
67
+ try {
68
+ d = decodeURIComponent(pathAndSearch);
69
+ }
70
+ catch {
71
+ return "/";
72
+ }
73
+ if (/[\x00-\x1f\x7f]/.test(d))
74
+ return "/";
75
+ if (!d.startsWith("/") || d.startsWith("//") || d.includes("\\"))
76
+ return "/";
77
+ try {
78
+ if (new URL(d, "https://n.invalid").origin !== "https://n.invalid")
79
+ return "/";
80
+ }
81
+ catch {
82
+ return "/";
83
+ }
84
+ return pathAndSearch;
85
+ }
86
+ function segPrefix(path, p) {
87
+ const base = p.endsWith("/") ? p.slice(0, -1) : p;
88
+ if (base === "")
89
+ return path === "/"; // "/" means the root path only, not "every path"
90
+ return path === base || path.startsWith(base + "/");
91
+ }
92
+ function isPublic(path, list) {
93
+ return list.some((p) => (typeof p === "string" ? segPrefix(path, p) : new RegExp(p.source, p.flags.replace(/[gy]/g, "")).test(path)));
94
+ }
95
+ export function createAuthMiddleware(o) {
96
+ if (typeof o !== "object" || o === null)
97
+ throw new RelytAuthError("config", { option: "issuer" });
98
+ normalizeIssuer(o.issuer);
99
+ const verify = createVerifier(o);
100
+ if (typeof o.signInUrl !== "string" || o.signInUrl === "")
101
+ throw new RelytAuthError("config", { option: "signInUrl" });
102
+ try {
103
+ new URL(o.signInUrl, "https://placeholder.invalid");
104
+ }
105
+ catch {
106
+ throw new RelytAuthError("config", { option: "signInUrl" });
107
+ }
108
+ const signInUrl = o.signInUrl;
109
+ const apiPrefix = o.apiPrefix ?? "/api/";
110
+ if (typeof apiPrefix !== "string" || !apiPrefix.startsWith("/"))
111
+ throw new RelytAuthError("config", { option: "apiPrefix" });
112
+ /** "/api/" also covers the bare path "/api". */
113
+ const apiBare = apiPrefix.length > 1 && apiPrefix.endsWith("/") ? apiPrefix.slice(0, -1) : null;
114
+ const cookieName = cookieNameOf(o.cookieName);
115
+ const cookieOnApi = o.cookieOnApi ?? true;
116
+ const publicPaths = o.publicPaths ?? [];
117
+ return async (req) => {
118
+ const headers = new Headers();
119
+ req.headers.forEach((v, k) => {
120
+ if (!k.toLowerCase().startsWith(STRIP_PREFIX))
121
+ headers.set(k, v);
122
+ });
123
+ const pass = () => NextResponse.next({ request: { headers } });
124
+ const path = req.nextUrl.pathname;
125
+ if (isPublic(path, publicPaths))
126
+ return pass();
127
+ const api = path.startsWith(apiPrefix) || path === apiBare || (req.headers.get("accept") ?? "").toLowerCase().includes("application/json");
128
+ const token = readToken(req.headers, cookieName, !api || cookieOnApi);
129
+ let failure = "none";
130
+ if (token !== null) {
131
+ try {
132
+ await verify(token);
133
+ return pass();
134
+ }
135
+ catch (e) {
136
+ failure = e instanceof RelytAuthError && e.code === "jwks_unavailable" ? "unavailable" : "invalid";
137
+ }
138
+ }
139
+ if (failure === "unavailable") {
140
+ return NextResponse.json({ error: "auth_unavailable" }, { status: 503, headers: { "retry-after": "2", "cache-control": NO_STORE } });
141
+ }
142
+ if (api) {
143
+ const www = failure === "none" ? "Bearer" : 'Bearer error="invalid_token"';
144
+ return NextResponse.json({ error: "unauthorized" }, { status: 401, headers: { "www-authenticate": www, "cache-control": NO_STORE } });
145
+ }
146
+ const target = new URL(signInUrl, req.nextUrl.href);
147
+ target.searchParams.set("next", safeNext(path + req.nextUrl.search));
148
+ const method = req.method.toUpperCase();
149
+ const res = NextResponse.redirect(target, method === "GET" || method === "HEAD" ? 307 : 303);
150
+ res.headers.set("cache-control", NO_STORE);
151
+ return res;
152
+ };
153
+ }
154
+ /**
155
+ * Reads claims in a route handler: takes the token from the bearer header (preferred) or the cookie and verifies it again; never reads x-relyt-*.
156
+ * `cookie` defaults to true; false ⇒ Authorization only (matches the middleware's `cookieOnApi: false`).
157
+ * Missing / invalid / other-tenant token ⇒ null; JWKS unavailable ⇒ throws RelytAuthUnavailableError by default, `onUnavailable: "null"` ⇒ null.
158
+ */
159
+ export async function getClaims(req, o, opts = {}) {
160
+ const token = readToken(req.headers, cookieNameOf(o?.cookieName), opts.cookie ?? true);
161
+ if (token === null) {
162
+ normalizeIssuer(o?.issuer); // validate the config even without a token, so a misconfiguration is not hidden behind null
163
+ return null;
164
+ }
165
+ try {
166
+ return await verifyToken(token, o);
167
+ }
168
+ catch (e) {
169
+ if (!(e instanceof RelytAuthError))
170
+ throw e;
171
+ if (e.code === "jwks_unavailable") {
172
+ if ((opts.onUnavailable ?? "throw") === "null")
173
+ return null;
174
+ throw new RelytAuthUnavailableError();
175
+ }
176
+ if (e.code === "invalid_token" || e.code === "invalid_claims")
177
+ return null;
178
+ throw e;
179
+ }
180
+ }
181
+ /** Reads the payload `exp` only (no verification: the cookie is written by the server that just obtained the token, Max-Age is only a lifetime cap). */
182
+ function expOf(token) {
183
+ const exp = decodePayloadUnverified(token)?.exp;
184
+ return typeof exp === "number" && Number.isFinite(exp) ? exp : null;
185
+ }
186
+ const MAX_COOKIE_BYTES = 4000;
187
+ const COOKIE_ATTRS = { path: "/", secure: true, httpOnly: true, sameSite: "lax" };
188
+ /** Writes the token cookie: Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=min(TOKEN_TTL_S, exp − now); remaining ≤ 0 ⇒ clears it instead. */
189
+ export function setTokenCookie(res, token, o = {}) {
190
+ const name = cookieNameOf(o.cookieName);
191
+ const exp = expOf(token);
192
+ const left = exp === null ? 0 : Math.floor(exp - Date.now() / 1000);
193
+ if (left <= 0)
194
+ return clearTokenCookie(res, o);
195
+ const maxAge = Math.min(TOKEN_TTL_S, left);
196
+ // Browsers cap a single cookie at about 4096 bytes and silently drop larger ones ⇒ sign-in "succeeds" but no token ever arrives; fail loudly on the server instead.
197
+ const serialized = `${name}=${token}; Path=/; Max-Age=${maxAge}; Secure; HttpOnly; SameSite=Lax`;
198
+ if (new TextEncoder().encode(serialized).length > MAX_COOKIE_BYTES)
199
+ throw new RelytAuthError("config", { option: "cookieSize" });
200
+ res.cookies.set(name, token, { ...COOKIE_ATTRS, maxAge });
201
+ }
202
+ /** Clears the token cookie (sign-out must call this server-side: the cookie is HttpOnly, browser JS cannot clear it). */
203
+ export function clearTokenCookie(res, o = {}) {
204
+ res.cookies.set(cookieNameOf(o.cookieName), "", { ...COOKIE_ATTRS, maxAge: 0 });
205
+ }
@@ -0,0 +1,2 @@
1
+ /** Numeric option: undefined ⇒ dflt; non-finite or failing `ok` ⇒ `config` (carries the option name only, never the value). */
2
+ export declare function num(v: unknown, dflt: number, option: string, ok: (n: number) => boolean): number;
@@ -0,0 +1,10 @@
1
+ // Shared option validation (used by verify.ts, client.ts and social.ts).
2
+ import { RelytAuthError } from "./errors.js";
3
+ /** Numeric option: undefined ⇒ dflt; non-finite or failing `ok` ⇒ `config` (carries the option name only, never the value). */
4
+ export function num(v, dflt, option, ok) {
5
+ if (v === undefined)
6
+ return dflt;
7
+ if (typeof v !== "number" || !Number.isFinite(v) || !ok(v))
8
+ throw new RelytAuthError("config", { option });
9
+ return v;
10
+ }
@@ -0,0 +1,54 @@
1
+ import { type AuthClient } from "./client.js";
2
+ /** sessionStorage key: one pending sign-in record `{v, mode, base, t}` (single use, removed when read by complete). */
3
+ export declare const SOCIAL_PKCE_STORAGE_KEY = "relyt.auth.social.pkce";
4
+ export type SocialResponseMode = "fragment" | "query";
5
+ export interface StartSocialSignInOptions {
6
+ /** `https://<authHost>`, same rules as createAuthClient. */
7
+ authUrl: string;
8
+ provider: string;
9
+ /** Must match the callback URL registered in the console verbatim (exact match, no wildcards). */
10
+ callbackURL: string;
11
+ errorCallbackURL?: string;
12
+ newUserCallbackURL?: string;
13
+ /** Default fragment (the code never reaches server logs / Referer). */
14
+ responseMode?: SocialResponseMode;
15
+ }
16
+ export interface CompleteSocialSignInOptions {
17
+ authUrl: string;
18
+ /** Called with the session token (bearer mode); the app stores it and provides it via createAuthClient's sessionToken. */
19
+ onSessionToken: (t: string) => void;
20
+ /** Given ⇒ clear() is called before delivering the new token, dropping JWTs cached for the previous identity. */
21
+ client?: AuthClient;
22
+ fetch?: typeof fetch;
23
+ timeoutMs?: number;
24
+ }
25
+ export interface ExchangeHandoffCodeOptions {
26
+ authUrl: string;
27
+ code: string;
28
+ codeVerifier: string;
29
+ fetch?: typeof fetch;
30
+ timeoutMs?: number;
31
+ }
32
+ export interface SocialStart {
33
+ /** Full URL of the auth host's sign-in start endpoint (navigate the top-level window to it). */
34
+ url: string;
35
+ /** PKCE verifier; submitted verbatim on exchange, never logged. */
36
+ codeVerifier: string;
37
+ }
38
+ /**
39
+ * Pure builder (works in browsers and Node): returns the start URL and the verifier; the caller keeps the verifier and navigates.
40
+ * Used for the server-proxied flow: the server generates it, binds the verifier to its own session, and redeems the code with exchangeHandoffCode after the callback.
41
+ */
42
+ export declare function createSocialStart(o: StartSocialSignInOptions): Promise<SocialStart>;
43
+ /** Browser entry: build → store the pending record → navigate the top-level window to the auth host. */
44
+ export declare function startSocialSignIn(o: StartSocialSignInOptions): Promise<void>;
45
+ /**
46
+ * Call on the registered callback page: clean the address bar → validate options → take the verifier → exchange → deliver the session token.
47
+ * Re-entrant: a call while one is in progress (e.g. React StrictMode's double effect) returns the same promise, so the code is exchanged
48
+ * and the token delivered exactly once; options passed to the later call are ignored.
49
+ */
50
+ export declare function completeSocialSignIn(o: CompleteSocialSignInOptions): Promise<void>;
51
+ /** Server-side proxy (Node / Next route handler): no Origin header, exchanges directly. */
52
+ export declare function exchangeHandoffCode(o: ExchangeHandoffCodeOptions): Promise<{
53
+ sessionToken: string;
54
+ }>;