@fractalshq/auth-core
v0.1.12
Published
Pure, side-effect free helpers for SIWS and action intents.
Readme
@fractalshq/auth-core
Pure, side-effect free helpers for SIWS and action intents.
API
import { core } from "@fractalshq/auth-core";
const { issuedAt, expirationTime } = core.timeWindow(new Date(), 300);
const message = core.generateSiwsMessage({
domain: "example.com",
address: "<pubkey>",
statement: "Sign in",
uri: "https://example.com",
nonce: core.randomNonce(),
issuedAt,
expirationTime,
});
const ok = core.verifySiwsSignature({ publicKey: "<pubkey>", signature: "<sig>", message });
// Ledger / transaction-based flow: verify against serialized message bytes
const okLedger = core.verifySiwsSignature({
publicKey: "<pubkey>",
signature: "<sig>",
messageBytes: "BASE64_SERIALIZED_MESSAGE",
messageEncoding: "base64",
});
// Ledger memo helper: build memo payload and validate memo transaction payloads
const parsed = core.verifyMemoAuthSignature({
publicKey: "<pubkey>",
signature: "<sig>",
messageBytes: "BASE64_SERIALIZED_MESSAGE",
messageEncoding: "base64",
expectedMemo: core.generateMemoAuthPayload({
nonce: "<nonce>",
issuedAt,
expirationTime,
domain: "example.com",
}),
});
console.log(parsed.memo, parsed.recentBlockhash);
console.log(parsed.payload?.nonce);Constants and roles
import {
API_PREFIX,
AUTH_BASE,
ORGS_BASE,
ME_ENDPOINT,
FRACTALS_PLATFORM_ID,
DISTRIBUTION_SAAS_PLATFORM_ID,
FRACTALS_ORGANIZATIONS_ID,
DISTRIBUTION_SAAS_ORGANIZATIONS_ID,
PLATFORM_ROLE_KEYS,
} from "@fractalshq/auth-core";SIWS Driver
Create a driver that talks to your auth service. baseURL can be the origin or include /api/v1/auth; it is normalized internally.
import { createSiwsDriver } from "@fractalshq/auth-core";
const driver = createSiwsDriver({
baseURL: process.env.AUTH_BASE_URL, // e.g. https://auth.example.com or https://auth.example.com/api/v1/auth
getApiKey: () => process.env.AUTH_INTERNAL_API_KEY || null,
});Client-side helpers
These throw on non-2xx and return parsed payloads:
// 1) Create a SIWS message
const { message } = await driver.createNonce("<pubkey>");
// 2) Sign message with wallet (client)
// ... get signature as base58 string ...
// 3) Complete sign-in
// For Ledger/tx flows, pass messageBytes (e.g., base64-encoded serialized message)
const { userId } = await driver.signInWithWallet({
pubkey: "<pubkey>",
signature: "<sig>",
message,
// messageBytes: base64Serialized,
// messageEncoding: "base64",
});Server/API route helpers (UpstreamResponse)
When proxying through your Next.js/express route and you need to forward status and Set-Cookie, use the UpstreamResponse (or HttpResponse) variants. They do not throw on non-2xx and include headers to forward.
// Create nonce and forward upstream response as-is
const { status, bodyText, contentType, setCookie } = await driver.createNonceUpstreamResponse("<pubkey>");
const res = new Response(bodyText, { status, headers: { "content-type": contentType || "application/json" } });
if (setCookie) (res.headers as any).set("set-cookie", setCookie as any);
// Sign-in and forward cookies
const r = await driver.signInWithWalletUpstreamResponse({ pubkey, signature, message, organizationId });Aliases:
createNonceUpstreamResponse=createNonceHttpResponse=createNonceRawsignInWithWalletUpstreamResponse=signInWithWalletHttpResponse=signInWithWalletRaw
One-file Next.js Route (framework-agnostic handler)
Use a single catch-all route that forwards cookies/headers to your auth service and supports safe redirects. Handlers use Web Request/Response, so they are portable.
- Create
app/api/v1/auth/siws/[...siws]/route.tsin your app:
import { siwsRouteHandler } from "@fractalshq/auth-core";
import type { SiwsRouteOptions } from "@fractalshq/auth-core";
const options: SiwsRouteOptions = {
// Optional: allow redirects via ?redirectTo=
redirectParam: "redirectTo",
// Comma-separated env of allowed cross-origin redirects (host[:port])
allowedRedirectHosts: (process.env.ALLOWED_REDIRECT_HOSTS || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean),
};
export async function POST(req: Request) {
return siwsRouteHandler(req, options);
}- Optionally re-export specific endpoints (e.g.,
create-nonce) to keep nice paths:
// app/api/v1/auth/siws/create-nonce/route.ts
export { POST } from "../[...siws]/route";meandsessionsroutes using SDK handlers:
// app/api/v1/me/route.ts
export { meRouteGET as GET } from "@fractalshq/auth-core";
// app/api/v1/sessions/route.ts
export { sessionsRouteGET as GET, sessionsRouteDELETE as DELETE } from "@fractalshq/auth-core";- Required env variables:
# Auth service origin (or /api/v1/auth path, both work)
AUTH_BASE_URL=https://auth.example.com
# Optional: internal key forwarded as x-internal-key
AUTH_INTERNAL_API_KEY=your-internal-key
# Optional: allowlist hosts for absolute redirects
ALLOWED_REDIRECT_HOSTS=app.example.com,another.example.com:3000Notes:
- Redirects use 303 and forward upstream Set-Cookie when present.
- You can still opt into Next-specific helpers via
NextResponse.from(...)if needed.
All-in-one Router (optional)
If you prefer a single route that handles SIWS, sessions, and me:
// app/api/v1/[...fractals-auth]/route.ts
import { fractalsAuthHandler } from "@fractalshq/auth-core";
export const GET = (req: Request) => fractalsAuthHandler(req, {
redirectParam: "redirectTo",
allowedRedirectHosts: (process.env.ALLOWED_REDIRECT_HOSTS || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean),
});
export const POST = (req: Request) => fractalsAuthHandler(req, {
redirectParam: "redirectTo",
allowedRedirectHosts: (process.env.ALLOWED_REDIRECT_HOSTS || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean),
});
export const DELETE = (req: Request) => fractalsAuthHandler(req);Supported paths:
/api/v1/auth/siws/create-nonce(POST)/api/v1/auth/siws/sign-in(POST)/api/v1/sessions(GET, DELETE)/api/v1/me(GET)
Role helpers usage
Protect routes and check roles with server helpers.
// app/api/admin/route.ts
import { requirePlatformRole, FRACTALS_PLATFORM_ID } from "@fractalshq/auth-core";
export async function GET(req: Request) {
const forbidden = await requirePlatformRole(req, FRACTALS_PLATFORM_ID, ["admin"]);
if (forbidden) return forbidden;
return Response.json({ ok: true });
}// app/api/org/[id]/manage/route.ts
import { requireOrgRole } from "@fractalshq/auth-core";
export async function POST(req: Request, { params }: { params: { id: string } }) {
const forbidden = await requireOrgRole(req, params.id, ["admin", "distributor"]);
if (forbidden) return forbidden;
return Response.json({ ok: true });
}// app/api/me/route.ts
import { getMeFromRequest } from "@fractalshq/auth-core";
export async function GET(req: Request) {
const me = await getMeFromRequest(req);
if (!me) return new Response(null, { status: 204 });
return Response.json(me);
}// app/api/guarded/route.ts
import { withRequiredRole, requireOrgRole } from "@fractalshq/auth-core";
async function handler(_req: Request) {
return Response.json({ ok: true });
}
export const GET = withRequiredRole(handler, (req) => requireOrgRole(req, "<org-id>", ["admin"]));