@finueva/auth
v0.31.0
Published
Public browser and server SDK for Finueva Auth
Readme
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
pnpm add @finueva/authFor 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.
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 for the assertion contract and limits.
Browser Quick Start
Create the client with the exact Auth and application origins:
import { AuthError, createAuthClient } from "@finueva/auth";
const auth = createAuthClient({
authOrigin: "https://auth.finueva.com",
appOrigin: window.location.origin,
});Read the current session:
const session = await auth.getSession();
if (session) {
console.log(`Signed in as ${session.user.email}`);
}Sign in with email and password:
try {
const session = await auth.signIn({
email: "[email protected]",
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() |
| Complete a second-factor sign-in | verifyTwoFactor() | Not available | Device authorization |
| Manage two-step verification | startTwoFactorEnrollment(), confirmTwoFactorEnrollment(), disableTwoFactor(), regenerateTwoFactorBackupCodes() | Not available | Not available |
| Read two-step verification state | getTwoFactorStatus() | Not available | getTwoFactorStatus() |
| 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 |
| Add a member to a team | addOrganizationTeamMember() | addOrganizationTeamMember() (cookie) | Not available |
| Remove a member from a team | removeOrganizationTeamMember() | removeOrganizationTeamMember() (cookie) | Not available |
| Delete an ordinary team | deleteOrganizationTeam() | deleteOrganizationTeam() (cookie) | Not available |
| Accept an invitation | acceptInvitation() | acceptInvitation() | Not available |
| Decline an invitation | declineInvitation() | declineInvitation() | 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() |
| List signed-in sessions | listSessions() | Not available | listSessions() |
| End other sessions | revokeSession(), revokeOtherSessions() | Not available | revokeSession(), revokeOtherSessions() |
| Start an Auth-Origin ceremony | createCeremonyHandoff() | Not available | createCeremonyHandoff() |
| List and rename passkeys | listPasskeys(), renamePasskey() | Not available | listPasskeys(), renamePasskey() |
| End the current session | signOut() | signOut() | revokeCurrentDevice() |
Create An Organization Invitation
Use one new idempotency key for each invitation that you intend to create:
const invitation = await auth.createOrganizationMemberInvitation("org_01J8R7M6N5P4Q3S2T1", {
email: "[email protected]",
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:
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:
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.
Manage Team Membership
Manual owners and manual administrators add a current organization member to an ordinary team, remove a member from a team, or delete an ordinary team. The three operations use browser-cookie or server-cookie authority:
await auth.addOrganizationTeamMember(organizationId, teamId, memberId, crypto.randomUUID());
await auth.removeOrganizationTeamMember(organizationId, teamId, memberId, crypto.randomUUID());
await auth.deleteOrganizationTeam(organizationId, teamId, crypto.randomUUID());Auth keeps the last active team, the caller's selected team, and a team that a pending invitation targets. Repeat an operation with the same key only after a lost response. A bearer-backed server client rejects the three methods before network I/O. The invitation recipient declines an invitation with declineInvitation({ invitationId }).
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:
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:
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:
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:
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:
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:
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:
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:
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().
Recover A Google Account Conflict
Use the request-scoped server client. If Google sign-in reports an account conflict, call startGoogleAccountRecovery({ idToken: { token, nonce } }). Auth verifies the Google identity and confirms that an existing verified password account requires linking. A generic sign-in error alone does not authorize recovery.
The result contains an opaque recoveryToken and an ISO expiresAt. Keep the result and Google ID token in short-lived encrypted server-managed state. Do not put them in URLs, logs, or browser storage.
Ask the user to sign in to the existing account with its password. Complete the configured second factor. Then create a new request-scoped client with that session and call completeGoogleAccountRecovery({ recoveryToken, idToken: { token, nonce } }). Auth requires a fresh password authentication after the recovery request. An old session or a different account cannot complete recovery. The result is { linked: true }; the current session remains in use.
If completion returns google_recovery_in_progress, retain the current session and recovery state. A retry can confirm a committed link. Auth does not repeat an uncertain linking mutation. If the original attempt cannot finish, wait for its ten-minute expiry before restarting.
Recovery is cookie-only and bound to the registered app origin. It does not enable implicit linking or link users by an email match alone. If recovery expires, restart Google sign-in. If email verification is required, complete the existing account's verification first.
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.
import { createDriveRecipientAuth } from "@finueva/auth/server";
const recipients = createDriveRecipientAuth({
authOrigin: "https://auth.finueva.com",
timeoutMs: 10_000,
});
const recipient = await recipients.resolveRecipient(
{ email: "[email protected]" },
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:
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:
import { AuthError } from "@finueva/auth";
try {
await auth.signIn({
email: "[email protected]",
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.
timeoutMsaccepts 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
memberrole 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
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
- Browser applications
- Server and SSR applications
- CLI and native applications
- Social ID-token sign-in
- Organizations and invitations
- Organization admission
- Drive credentials
- Errors and recovery
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.
Organization presence authorization candidate
The request-scoped server method authorizeOrganizationPresence(organizationId, { applicationId, userIds }, serviceAssertion, { applicationCredential }) forwards fresh actor, application and registered service proofs. The exact ordered selection contains 1 to 50 Auth User IDs. The supplied application credential remains untrusted proof. Auth checks its signature and exact current actor session. The result binds the actor, organization, application and selected IDs; its observation time is not an authority lease. Your application must enforce its own disclosure and visibility policies. Browser and device clients have no direct service facade. This source candidate requires release and registration qualification before use.
Organization admission candidate
Import createOrganizationAdmissionAuth, createOrganizationAdmissionBillingAuth, and createOrganizationAdmissionDirectoryAuth from @finueva/auth/server. These separate clients expose actor setup operations, private billing attestations, and the pending setup directory. Actor operations require a current incoming request and a separate service assertion. Use organizationAdmissionBodyDigest(operation, input) and the client's audience(operation) when signing each fresh assertion. A pending setup grants no organization membership or Drive authority. This source candidate requires release and hosted qualification before use.
