# Finueva Auth SDK for TypeScript

Use `@finueva/auth` to add Finueva sign-in, organization access, invitations, and Drive credentials to a browser, server, CLI, or native application.

The current published version is `0.11.1`. The package is ESM-only. Node.js consumers require Node.js 22 or later. Other server runtimes must provide compatible Fetch, Web Crypto, URL, and Web Streams APIs. Its interface is Experimental until version `1.0.0`.

The platform-status, organization-authority, and invitation-administration methods below describe this repository's next package candidate. Published version `0.11.1` does not include them.

## Install

```sh
pnpm add @finueva/auth
```

For a browser or server application, ask your Auth operator for:

- the Auth origin for your environment;
- your registered application origin.

For a CLI or native application, ask for:

- the Auth origin for your environment;
- a registered device client ID.

Do not reuse configuration from another application or environment.

## Choose A Client

| Runtime                   | Import                 | Session authority              | Create it                |
| ------------------------- | ---------------------- | ------------------------------ | ------------------------ |
| Browser                   | `@finueva/auth`        | Auth HTTP-only cookie          | Once for the application |
| Server or SSR handler     | `@finueva/auth/server` | Incoming Auth cookie or bearer | Once per request         |
| CLI or native application | `@finueva/auth/device` | Device bearer session          | Once per process         |

## Private Identity Resolution Candidate

For private server-to-server identity resolution, the next package candidate exports `createIdentityResolverAuth` from `@finueva/auth/server`. This helper is not in published `0.15.0`.

```ts
import { createIdentityResolverAuth } from "@finueva/auth/server";

const identities = createIdentityResolverAuth({ authOrigin: "https://auth.example.test" });

async function resolvePrivateIdentity(identifier: string, clientAssertion: string) {
  return identities.resolveIdentity({ identifier }, clientAssertion);
}
```

The approved server signs a fresh assertion with its registered key and explicit `identities:resolve` capability. The method returns only `{ userId }` or null for one exact username or email. Keep the result private. It grants no product authority. See the [server guide](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/server.md#resolve-a-private-identity) for the assertion contract and limits.

## Browser Quick Start

Create the client with the exact Auth and application origins:

```ts
import { AuthError, createAuthClient } from "@finueva/auth";

const auth = createAuthClient({
  authOrigin: "https://auth.finueva.com",
  appOrigin: window.location.origin,
});
```

Read the current session:

```ts
const session = await auth.getSession();

if (session) {
  console.log(`Signed in as ${session.user.email}`);
}
```

Sign in with email and password:

```ts
try {
  const session = await auth.signIn({
    email: "ada@example.com",
    password: "correct-horse-battery-staple",
  });

  console.log(`Welcome ${session.user.name}`);
} catch (error) {
  if (error instanceof AuthError && error.code === "invalid_credentials") {
    showInvalidCredentials();
  } else {
    throw error;
  }
}
```

The browser owns the session cookie. The SDK does not expose or persist it.

## Common Tasks

| Task                            | Browser                                     | Server                                    | Device                                 |
| ------------------------------- | ------------------------------------------- | ----------------------------------------- | -------------------------------------- |
| Create an account               | `signUp()`                                  | Not available                             | `signUpWithEmail()`                    |
| Read the current session        | `getSession()`                              | `getSession()`                            | `getSession()`                         |
| Observe session state           | `onAuthStateChange()`                       | Not available                             | Not available                          |
| Update a profile                | `updateProfile()`                           | Not available                             | `updateProfile()`                      |
| Change a password               | `changePassword()`                          | Not available                             | `changePassword()`                     |
| Reset a password                | `requestPasswordReset()`, `resetPassword()` | Not available                             | Not available                          |
| Verify an email                 | `sendVerificationEmail()`                   | Not available                             | Not available                          |
| List organizations              | `listOrganizations()`                       | `listOrganizations()`                     | `listOrganizations()`                  |
| Read member authority           | `getOrganizationMemberAuthority()`          | `getOrganizationMemberAuthority()`        | `getOrganizationMemberAuthority()`     |
| Create an ordinary organization | `createOrganization()`                      | `createOrganization()`                    | `createOrganization()`                 |
| Create an ordinary team         | `createOrganizationTeam()`                  | `createOrganizationTeam()`                | `createOrganizationTeam()`             |
| Invite an ordinary member       | `createOrganizationMemberInvitation()`      | `createOrganizationMemberInvitation()`    | `createOrganizationMemberInvitation()` |
| Resend a member invitation      | `resendOrganizationInvitation()`            | `resendOrganizationInvitation()` (cookie) | Not available                          |
| Cancel a member invitation      | `cancelOrganizationInvitation()`            | `cancelOrganizationInvitation()` (cookie) | Not available                          |
| Remove an ordinary member       | `removeOrganizationMember()`                | `removeOrganizationMember()` (cookie)     | Not available                          |
| Leave an organization           | `leaveOrganization()`                       | `leaveOrganization()` (cookie)            | Not available                          |
| Accept an invitation            | `acceptInvitation()`                        | `acceptInvitation()`                      | Not available                          |
| Enroll a passkey                | `addPasskey()`                              | Not available                             | Not available                          |
| Perform recent authentication   | Platform and organization action methods    | Not available                             | Not available                          |
| Change manual roles or owners   | Organization authority methods              | Not available                             | Not available                          |
| Read current platform roles     | `getPlatformStatus()`                       | Not available                             | Not available                          |
| Request Drive access            | `getDriveCredential()`                      | `getDriveCredential()`                    | `getDriveCredential()`                 |
| End the current session         | `signOut()`                                 | `signOut()`                               | `revokeCurrentDevice()`                |

## Create An Organization Invitation

Use one new idempotency key for each invitation that you intend to create:

```ts
const invitation = await auth.createOrganizationMemberInvitation("org_01J8R7M6N5P4Q3S2T1", {
  email: "member@example.com",
  teamIds: ["team_01J8R7M6N5P4Q3S2T1"],
  idempotencyKey: crypto.randomUUID(),
});

console.log(invitation.id, invitation.expiresAt);
```

If the response is lost, repeat the same request with the same key. The SDK does not retry automatically. The result confirms invitation creation. It does not confirm email delivery.

Manual owners and manual administrators can resend or cancel the exact pending invitation through browser-cookie or server-cookie authority:

```ts
const resent = await auth.resendOrganizationInvitation(organizationId, invitation.id, crypto.randomUUID());
const canceled = await auth.cancelOrganizationInvitation(organizationId, invitation.id, crypto.randomUUID());
```

Resend success confirms the committed expiry. It does not confirm email delivery. Repeat an action with the same key only after a lost response. A bearer-backed server client rejects both methods before network I/O.

## Remove An Organization Member

Manual owners and manual administrators remove one other member. Every current member can leave. Both operations use browser-cookie or server-cookie authority:

```ts
const removed = await auth.removeOrganizationMember(organizationId, memberId, crypto.randomUUID());
const left = await auth.leaveOrganization(organizationId, crypto.randomUUID());
```

An owner target is refused. Transfer ownership first. Repeat a removal with the same key only after a lost response; a membership that is already gone under that key replays the same result. A bearer-backed server client rejects both methods before network I/O.

## Perform Recent Authentication

The experimental passkey methods run only when the browser page uses the exact Auth Origin. To enroll a passkey, first create a new password-authenticated session on that origin:

```ts
await auth.signIn({ email, password });
await auth.addPasskey();
```

The enrollment authorization expires after five minutes. It is single-use. Signup, social sign-in, session refresh, bearer use, and generic session creation do not authorize enrollment. A User without a fresh password sign-in cannot use this path.

Start one reviewed platform-role action before invoking passkey authentication:

```ts
await auth.startPlatformRoleChangeRecentAuthentication({
  targetUserId: "user_01J8R7M6N5P4Q3S2T1",
  requestedRoles: ["administrator", "investigator"],
  expectedTargetRevision: 0,
  expectedGlobalRevision: 0,
  reasonCategory: "initial_bootstrap",
  changeReference: "change_01J8R7M6N5P4Q3S2T1",
  idempotencyKey: crypto.randomUUID(),
});

await auth.verifyRecentAuthenticationWithPasskey();

const continuation = await auth.getRecentAuthenticationStatus();
if (continuation.status === "ready") {
  showReviewedActionIsReady();
}
```

The maintained passkey client performs the WebAuthn ceremony for the pending action when a compatible authenticator is present. Auth binds the exact generated challenge to that action. The SDK returns only `pending` or `ready`. It does not expose the challenge, WebAuthn response, session, cookies, proof, or hashes. `ready` does not grant a platform role or reusable administration mode. Bootstrap approval consumes the proof atomically with its state transition.

Organization owner changes use the same ceremony. Pass the same input to the start and submit methods:

```ts
const change = {
  requestedManualRole: "owner" as const,
  expectedActorAuthorityRevision: 3,
  expectedTargetAuthorityRevision: 1,
  idempotencyKey: crypto.randomUUID(),
};

await auth.startOrganizationOwnerRoleChangeRecentAuthentication(organizationId, memberId, change);
await auth.verifyRecentAuthenticationWithPasskey();
await auth.changeOrganizationMemberManualRole(organizationId, memberId, change);
```

Atomic transfer similarly pairs `startOrganizationOwnershipTransferRecentAuthentication()` with `transferOrganizationOwnership()`. The input names distinct outgoing-owner and successor memberships, the outgoing owner's resulting `member` or `admin` role, all exact authority revisions, and one idempotency key. The SDK validates and sends one normalized request and never exposes the consumed proof.

If a start result is unavailable, call `startPlatformRoleChangeRecentAuthentication()` again with the same normalized input. The client reuses the current request nonce. A different start or observed account transition removes this local retry state. Only one start request can be active. An overlapping call returns `state_conflict` before network I/O. The SDK does not retry automatically.

Read the signed-in User's current effective platform roles on the exact Auth Origin:

```ts
const platform = await auth.getPlatformStatus();
```

The result contains `eligible`, `roles`, and `globalRevision`. Use it for display and mutation preparation only. Each platform operation checks current authority again. The browser SDK sends Auth-origin cookie credentials and an exact browser-generated `Origin`. It sends no `Authorization` header. Server and device clients do not expose this method.

## Request A Drive Credential

Request one short-lived credential for one workspace:

```ts
const credential = await auth.getDriveCredential({
  workspace: { type: "personal" },
});

await fetch("https://drive.finueva.com/api/files", {
  headers: {
    authorization: `${credential.tokenType} ${credential.token}`,
  },
});
```

For an organization workspace:

```ts
const credential = await auth.getDriveCredential({
  workspace: {
    type: "organization",
    organizationId: "org_01J8R7M6N5P4Q3S2T1",
  },
});
```

Browser and device clients cache one completed credential in memory. The server client does not cache credentials. The SDK never replays a failed Drive operation.

## Server Quick Start

Create one client inside each request handler:

```ts
import { createServerAuth } from "@finueva/auth/server";

export async function handleRequest(request: Request): Promise<Response> {
  const auth = createServerAuth({
    authOrigin: "https://auth.finueva.com",
    appOrigin: "https://app.heylomeet.com",
    request,
  });

  const session = await auth.getSession();

  return Response.json(
    { authenticated: session !== null, user: session?.user ?? null },
    { headers: { "cache-control": "no-store" } },
  );
}
```

If the incoming request has `Authorization`, the server client uses that bearer and forwards no cookies. Otherwise, it forwards only Auth cookies.

Some server methods return cookie changes. Apply them before you send the response:

```ts
const result = await auth.signOut();
const headers = new Headers({ location: "/signed-out" });

result.applyCookies(headers);

return new Response(null, { status: 303, headers });
```

Call `applyCookies()` after server `signOut()`, `acceptInvitation()`, and `signInWithSocialIdToken()`.

## Resolve A Drive Recipient

Use the separate server client. Before lookup, Drive must check the caller's current permission to manage the item's shares. Recipient resolution does not grant file access.

```ts
import { createDriveRecipientAuth } from "@finueva/auth/server";

const recipients = createDriveRecipientAuth({
  authOrigin: "https://auth.finueva.com",
  timeoutMs: 10_000,
});

const recipient = await recipients.resolveRecipient(
  { email: "ada@example.com" },
  clientAssertion, // Generate a new service assertion for this request.
);
```

The result contains only `{ userId, name, email }`. The SDK trims and lowercases the exact email input. It accepts no additional input fields. Email has a 320-byte UTF-8 limit. The JSON request has a 1024-byte limit. The response has a 4096-byte limit. The canonical ID contains 1 to 128 URL-safe characters. The nonempty name has a 1024-byte UTF-8 limit. A response email must be normalized and match the requested email.

The caller generates one Ed25519 assertion with `typ: "finueva-drive-recipient-assertion+jwt"` and the registered JWK-thumbprint `kid`. Set `aud` to the exact Auth endpoint, such as `https://auth.finueva.com/api/v1/auth/drive-share-recipients/resolve`. Set `iss` and `sub` to the same registered service ID. Include the registered `environment`, `ver: 1`, `principal_type: "ecosystem-service"`, a new bounded `jti`, and `exp = iat + 60` in seconds. Auth permits 30 seconds of clock tolerance. A provisioning assertion cannot replace this assertion. The SDK accepts no keys. Auth verifies the assertion; the SDK checks only its compact syntax and the 16 KiB bearer-header limit.

This client sends no cookies or Origin. It follows no redirects, retains no recipient cache, and makes no automatic retries. It permits one active request per client and no queue. The default deadline is 10 seconds. `timeoutMs` accepts an integer from 1 to 60000. If a custom transport ignores abort, the client rejects new calls until that transport settles. A custom `fetch` must preserve the request's credential and redirect restrictions.

After a lost response or failure, use a new assertion for an explicit retry. Auth can consume an assertion even when the request fails. Missing or ineligible recipients return `AuthError` with `recipient_unavailable` and status 404. Exact invalid-request responses map to `invalid_request` with status 400 or 413. Invalid assertions map to `authentication_required` with status 401. Rate limits map to `auth_request_rate_limited` with status 429 and `retryAfterSeconds` from 1 to 60. Invalid responses, transport failures, and service failures map to `identity_authority_unavailable` with status 503. An overlapping call returns `state_conflict` with status 409. Error messages contain no request values.

Malformed assertion syntax fails locally with `invalid_request` and status 400. An exact `405 METHOD_NOT_ALLOWED` response with message `METHOD_NOT_ALLOWED` maps to `invalid_request` with status 405. Other non-contract errors fail closed with status 503. The SDK does not expose raw Auth error messages.

The factory and recipient types exist only in `@finueva/auth/server`. The cookie and session client from `createServerAuth()` does not expose recipient resolution.

## Device Quick Start

Use the device flow for a CLI or native application that can open a system browser:

```ts
import { createDeviceAuth } from "@finueva/auth/device";

const auth = createDeviceAuth({
  authOrigin: "https://auth.finueva.com",
  clientId: "finueva-cli",
});

const authorization = await auth.requestAuthorization();

console.log(`Open ${authorization.verificationUriComplete}`);
console.log(`Code: ${authorization.userCode}`);

await auth.pollForToken(authorization);
```

`clientId` is public. `deviceCode` is not. Never log or display the device code.

Device tokens are memory-only by default. For persistence, supply a `DeviceTokenPersistence` adapter backed by an operating-system credential vault. Do not use local storage, a WebView cookie jar, or a plaintext file.

## Handle Errors

Constructor and selected account-input errors can throw `TypeError`. Validated SDK request failures throw `AuthError`. Device authorization can throw `DeviceAuthorizationError`:

```ts
import { AuthError } from "@finueva/auth";

try {
  await auth.signIn({
    email: "ada@example.com",
    password: "correct-horse-battery-staple",
  });
} catch (error) {
  if (!(error instanceof AuthError)) throw error;

  if (error.code === "invalid_credentials") {
    showInvalidCredentials();
  } else if (error.code === "identity_authority_unavailable") {
    showAuthUnavailable();
  } else {
    showAuthError(error.code);
  }
}
```

Use `error.code` for application behavior. Do not parse `error.message`. Except for RFC 8628 device polling, the SDK does not automatically repeat requests.

## Security Checklist

- Use HTTPS. Plain HTTP is accepted only on localhost.
- Register exact application origins.
- Create server clients per request.
- Apply returned server cookies before you send the response.
- Keep browser session snapshots display-only.
- Keep device sessions in an operating-system credential vault.
- Keep Drive credentials in memory.
- Protect server-side mutations against CSRF.
- Do not log cookies, bearer tokens, ID tokens, reset tokens, device codes, or complete Auth responses.

## Current Limits

- The package is Experimental until `1.0.0`.
- Requests time out after 10 seconds by default.
- `timeoutMs` accepts 1 through 60,000 milliseconds.
- Most responses are limited to 64 KiB. Organization read responses can use up to 256 KiB.
- Browser snapshots and bearer authorization values are limited to 16 KiB.
- Organization collections use page sizes from 1 through 100.
- Organization Drive context supports at most 50 current teams.
- Invitation creation supports only the `member` role in ordinary organizations.
- Invitation resend and cancellation require browser-cookie or server-cookie authority and a manual owner or manual administrator.
- Cross-root SSO, custom refresh tokens, and account deletion are not implemented.
- Production support gates are incomplete. Ask the Auth operator for the current environment status before adoption.

## Package Exports

```ts
import { AuthError, createAuthClient } from "@finueva/auth";
import { createServerAuth } from "@finueva/auth/server";
import { DeviceAuthorizationError, createDeviceAuth } from "@finueva/auth/device";
```

TypeScript declarations ship with the package. The public declarations do not expose Better Auth types.

## Documentation

Long-form task guides are maintained with the Auth source:

- [Get started](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/index.md)
- [Browser applications](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/browser.md)
- [Server and SSR applications](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/server.md)
- [CLI and native applications](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/device.md)
- [Social ID-token sign-in](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/social-sign-in.md)
- [Organizations and invitations](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/organizations.md)
- [Drive credentials](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/drive-credentials.md)
- [Errors and recovery](https://github.com/Ifkafin/auth/blob/main/apps/developer/docs/sdk/errors-and-recovery.md)

These links track current source and can describe a newer SDK version. Check the version at the start of the guide. Repository access is required until these guides have a public, versioned documentation deployment.
