npm package: @fractalshq/auth-core
Description: Pure, side-effect free helpers for SIWS and action intents.
Latest version: 0.1.12

# @fractalshq/auth-core

Pure, side-effect free helpers for SIWS and action intents.

## API

```ts
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

```ts
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.

```ts
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:

```ts
// 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.

```ts
// 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` = `createNonceRaw`
- `signInWithWalletUpstreamResponse` = `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.

1) Create `app/api/v1/auth/siws/[...siws]/route.ts` in your app:

```ts
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);
}
```

2) Optionally re-export specific endpoints (e.g., `create-nonce`) to keep nice paths:

```ts
// app/api/v1/auth/siws/create-nonce/route.ts
export { POST } from "../[...siws]/route";
```

3) `me` and `sessions` routes using SDK handlers:

```ts
// 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";
```

4) Required env variables:

```bash
# 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:3000
```

Notes:
- 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`:

```ts
// 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.

```ts
// 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 });
}
```

```ts
// 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 });
}
```

```ts
// 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);
}
```

```ts
// 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"]));
```