@finueva/auth 0.5.0 → 0.7.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/README.md +50 -4
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/device.d.ts +8 -2
- package/dist/device.js +616 -101
- package/dist/index.d.ts +2 -3
- package/dist/index.js +323 -102
- package/dist/server.d.ts +6 -3
- package/dist/server.js +127 -35
- package/dist/{types-HSdjQWMZ.d.ts → types-B1SZbs6Z.d.ts} +28 -1
- package/package.json +3 -2
- package/dist/errors-Crd93Rah.d.ts +0 -11
package/README.md
CHANGED
|
@@ -22,7 +22,13 @@ const stop = auth.onAuthStateChange((state) => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
const session = await auth.getSession();
|
|
25
|
+
await auth.signUp(email, password, name);
|
|
25
26
|
await auth.signIn({ email, password });
|
|
27
|
+
await auth.signInWithSocialIdToken({ provider: "google", idToken, nonce, signUp: false });
|
|
28
|
+
await auth.updateProfile({ name, image });
|
|
29
|
+
await auth.changePassword(currentPassword, newPassword); // revokes other sessions by default
|
|
30
|
+
await auth.requestPasswordReset(email, `${window.location.origin}/account/reset-password`);
|
|
31
|
+
await auth.resetPassword(token, newPassword);
|
|
26
32
|
await auth.sendVerificationEmail({
|
|
27
33
|
email,
|
|
28
34
|
callbackURL: `${window.location.origin}/account/email-verified`,
|
|
@@ -39,10 +45,16 @@ await auth.signOut();
|
|
|
39
45
|
stop();
|
|
40
46
|
```
|
|
41
47
|
|
|
42
|
-
The Auth session uses the Auth-owned HTTP-only cookie. `getSession()` and `onSessionChange()`
|
|
48
|
+
The Auth session uses the Auth-owned HTTP-only cookie. `getSession()` reports one bounded Better Auth authority result. Every authoritative session read receives a monotonic epoch, and account transitions advance that epoch again; only the newest read may update state or persistence, while a delayed older caller receives value-free `state_conflict`. `onSessionChange()` starts authoritative loading by itself and is an SDK-owned projection: it emits only sessions/null accepted through that bounded path, never Better Auth client cache state or persisted hints. The SDK subscribes once to Better Auth's session atom only as an invalidation source; focus, online, local mutation, and cross-tab storage signals trigger a coalesced bounded SDK `get-session` refresh, while raw `useSession.data` is ignored. Signals wait during account mutation, and the current sign-out signal cycle is suppressed so prior authority cannot race the owning transition. Better Auth `1.6.22` broadcasts sign-out between tabs but not sign-in, so a background tab observes sign-in on its next focus or online invalidation. Listener exceptions are isolated from authority work and other listeners. `onAuthStateChange()` separately emits `loading`, non-authoritative `cached`, authoritative `authenticated`/`unauthenticated`, or non-authoritative `unavailable`; it verifies every cache hint immediately.
|
|
43
49
|
|
|
44
50
|
`sendVerificationEmail({ email, callbackURL? })` sends one non-retried request. An explicit callback must be an exact absolute URL on `appOrigin`; it is never persisted by the SDK. Without one, the email lands on Auth's value-free verification result page after Better Auth completes verification.
|
|
45
51
|
|
|
52
|
+
`signUp(email, password, name)`, `signIn({ email, password })`, `signInWithSocialIdToken({ provider, idToken, nonce?, signUp })`, `updateProfile({ name?, image? })`, and `changePassword(currentPassword, newPassword, revokeOtherSessions?)` delegate session ownership to Better Auth and return only the subsequently refreshed normalized session. Social providers are closed to `google | apple`, `signUp` is required, the ID token is copied and limited to 16 KiB, and an optional nonce is copied and limited to 256 printable ASCII bytes. At mutation start these methods synchronously invalidate the prior session ID, persisted-snapshot projection, auth-state and session-listener projections, and Drive authority. Physical browser-persistence removal is bounded best-effort asynchronous work. Only the bounded post-mutation refresh can publish the next session or null; refresh failure leaves the prior account absent and state unavailable. They never expose Better Auth's raw session token. Emails are trimmed and lowercased, names are trimmed, existing passwords accepted for sign-in or current-password proof contain 8 to 128 characters, new passwords require 10 to 128 characters, and a profile image must be `null` or one exact HTTPS URL (HTTP is accepted only on localhost). Profile updates require at least one field. SCIM-managed names remain provider-owned and cannot be changed through this method; image-only updates remain available.
|
|
53
|
+
|
|
54
|
+
`changePassword` defaults `revokeOtherSessions` to `true`, so Better Auth replaces the current session after removing the others. `requestPasswordReset(email, redirectTo)` requires an exact absolute callback on this client's `appOrigin`; the backend returns the same value-free success whether the account exists or not. `resetPassword(token, newPassword)` consumes Better Auth's one-time token, revokes existing sessions, and refreshes local authority. Password reset can therefore succeed even when a later session refresh is unavailable; do not automatically replay a consumed reset token.
|
|
55
|
+
|
|
56
|
+
`signOut()` returns `state_conflict` without making a request while an account mutation is active. Wait for that mutation to settle before signing out so a delayed mutation response cannot apply a later session cookie after a completed sign-out.
|
|
57
|
+
|
|
46
58
|
DOM browsers use local storage by default. Set `persistence` to `"session"`, `"memory"`, or a structural driver with `getItem`, `setItem`, and `removeItem`; Workers and Node default to memory. The one 16 KiB versioned snapshot contains only a normalized session and its Auth authority identity. It expires with the session and never contains cookies, passwords, Drive credentials, tokens, headers, workspace selection, Better Auth payloads, or errors. Persistence failure cannot change an Auth request result.
|
|
47
59
|
|
|
48
60
|
A workspace is either `{ type: "personal" }` or `{ type: "organization", organizationId }`; organization IDs must match `[A-Za-z0-9_-]{1,128}`. Workspace selections are strict and reject additional properties locally with `invalid_workspace_selection` before making a request.
|
|
@@ -75,7 +87,26 @@ const responseHeaders = new Headers();
|
|
|
75
87
|
signOut.applyCookies(responseHeaders);
|
|
76
88
|
```
|
|
77
89
|
|
|
78
|
-
The server client is request-scoped and does not cache credentials.
|
|
90
|
+
The server client is request-scoped and does not cache credentials. Without `Authorization`, it forwards only reserved Better Auth cookies to Auth and never sends them to Drive. When the incoming request contains one valid, at-most-16-KiB `Bearer` authorization value, every Auth call forwards that exact header and omits all cookies; bearer authority always wins a mixed request. Server social ID-token sign-in therefore rejects locally with value-free `state_conflict` and makes no request when the incoming request has `Authorization`, rather than switching from bearer to newly minted cookie authority. Malformed or oversized authorization fails locally with value-free `authentication_required` and never falls back to a cookie. Apply cookies returned by server social sign-in, invitation acceptance, or sign-out to the outgoing response with the result's `applyCookies(headers)` method.
|
|
91
|
+
|
|
92
|
+
### Server-Owned Google Callback
|
|
93
|
+
|
|
94
|
+
HeyloMeet's existing BFF may retain ownership of Google authorization, state, PKCE, and callback handling. After its maintained OAuth client exchanges the code with Google, pass only the returned Google ID token and the original nonce to Auth. Auth independently verifies the token, returns a normalized authoritative Better Auth session plus cookies, and never redirects to Google or returns provider/session tokens to the browser:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const auth = createServerAuth({ authOrigin, appOrigin, request });
|
|
98
|
+
const signedIn = await auth.signInWithSocialIdToken({
|
|
99
|
+
provider: "google",
|
|
100
|
+
idToken: googleTokenSet.id_token,
|
|
101
|
+
nonce: callbackNonce,
|
|
102
|
+
signUp: false,
|
|
103
|
+
});
|
|
104
|
+
const headers = new Headers({ location: "/" });
|
|
105
|
+
signedIn.applyCookies(headers);
|
|
106
|
+
return new Response(null, { status: 302, headers });
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Set `signUp: true` only for an explicit product signup action. Do not put the Google ID token, Better Auth cookie, or Better Auth session token in the redirect URL, application cookie, logs, or page state. Do not forward Google access or refresh tokens to Auth.
|
|
79
110
|
|
|
80
111
|
## Device
|
|
81
112
|
|
|
@@ -90,17 +121,32 @@ const authorization = await auth.requestAuthorization();
|
|
|
90
121
|
showCode(authorization.userCode, authorization.verificationUri);
|
|
91
122
|
const token = await auth.pollForToken(authorization, { signal });
|
|
92
123
|
const session = await auth.getSession();
|
|
124
|
+
await auth.signUpWithEmail(email, password, name); // direct first-party mobile flow
|
|
125
|
+
await auth.signInWithEmail(email, password);
|
|
126
|
+
await auth.signInWithSocialIdToken({ provider: "apple", idToken, nonce: rawNonce, signUp: false });
|
|
127
|
+
await auth.updateProfile({ name, image });
|
|
128
|
+
await auth.changePassword(currentPassword, newPassword);
|
|
129
|
+
const credential = await auth.getDriveCredential({ workspace: { type: "personal" } });
|
|
93
130
|
await auth.revokeCurrentDevice(); // or revokeAllSessions()
|
|
94
131
|
```
|
|
95
132
|
|
|
96
|
-
Polling starts at the server-provided interval, adds five seconds after each RFC 8628 `slow_down`, has no transport retry, and stops on denial, expiry, cancellation, the configured `maxPollingMs`, malformed/bounded responses, or transport timeout. The only requested scope is `openid`.
|
|
133
|
+
Polling starts at the server-provided interval, adds five seconds after each RFC 8628 `slow_down`, has no transport retry, and stops on denial, expiry, cancellation, the configured `maxPollingMs`, malformed/bounded responses, or transport timeout. A bounded access token recoverable from otherwise malformed redemption metadata is best-effort revoked and no local authority is retained. The only requested scope is `openid`.
|
|
134
|
+
|
|
135
|
+
The token is memory-only by default. Secure persistence is opt-in through a structural adapter implementing `read`, atomic `replace`, and `delete`; every operation receives an `AbortSignal`. The SDK serializes polling, account changes, expiry synchronization, and revocation, including persistence calls. Malformed non-null persisted authority is best-effort deleted before hydration fails, allowing a later process to start without the bad value. The caller owns encryption and platform credential storage. Adapter and authority failures are value-free and never include device codes or tokens. This export uses standard expiring/revocable Better Auth sessions and does not implement refresh tokens.
|
|
136
|
+
|
|
137
|
+
Direct email and social signup/sign-in are intended for registered first-party mobile or Tauri clients. The SDK verifies the opaque token through bearer `get-session`, derives its expiry from that authoritative session, atomically replaces secure persistence, and only then publishes it in memory. Verification or replacement failure triggers best-effort revocation of the new session and retains the previous token only after current authority verifies it. A successful account switch best-effort revokes the replaced current-device session after publishing the new token. Password replacement follows the same rule; when Better Auth returns no replacement token, the current token is retained only after an authoritative session check. `getSession()` synchronizes changed authoritative expiry back to persistence.
|
|
138
|
+
|
|
139
|
+
For Tauri, implement `DeviceTokenPersistence` in Rust commands backed by the operating system credential vault (Keychain, Credential Manager, or Secret Service). Store the complete `DeviceToken` as one encrypted/versioned item, make replacement atomic, and restrict access to this application identity. An `AbortSignal` cannot cancel a native `invoke` that has already been dispatched. The SDK retains an internal persistence queue after public timeout, runs compensating deletion after a late replacement, and rejects later authority operations with `state_conflict` until that queue settles. The process can still terminate between a native commit and compensation; production adapters need a native pending-operation journal or equivalent startup cleanup before returning a stored token. This process-crash recovery remains outside SDK qualification. Do not use WebView local storage, a plaintext file, Tauri Store, logs, crash metadata, or frontend state. Drive credentials remain memory-only even when the Better Auth session uses secure OS persistence; the device client keeps only one selected workspace credential and renews it with two minutes remaining. It permits at most one issuance request: an exact same-workspace non-forced call may share that request, while a workspace switch or forced refresh returns `state_conflict` until it settles.
|
|
97
140
|
|
|
98
|
-
|
|
141
|
+
Tauri must also supply `createDeviceAuth({ fetch })` with a native HTTP adapter or move the complete Auth transport into Rust. Native Google or Apple authorization submits the provider ID token through `signInWithSocialIdToken`; the adapter must emit neither WebView cookies nor a WebView `Origin`, and every device SDK request uses `credentials: "omit"`. The default WebView fetch path is unsupported for `tauri://` applications and environments that send `Origin: null`. Device authorization remains available by opening `verificationUriComplete` in the system browser.
|
|
99
142
|
|
|
100
143
|
## Current Limits
|
|
101
144
|
|
|
102
145
|
- Organization mutation and active-organization state are not exposed; callers select an organization returned by `listOrganizations()` explicitly.
|
|
103
146
|
- Cross-root SSO is not implemented.
|
|
147
|
+
- Cross-root redirect SSO and generic Better Auth Google/Apple social redirects are unsupported; an application or native provider SDK must obtain the ID token.
|
|
148
|
+
- Apple first signup without a verified signed email is unsupported. Requests with caller-supplied Apple email/name fields are rejected; a later email-less token works only for an already-linked Apple subject with a current verified Auth email.
|
|
104
149
|
- The package does not persist Drive credentials or expose Better Auth or Unstorage internals.
|
|
150
|
+
- Account deletion is intentionally not exposed by browser, server, or device exports.
|
|
105
151
|
- Public errors are `AuthError` instances with stable, value-free codes.
|
|
106
152
|
- Organization contexts with more than 50 current teams fail with `organization_context_too_large` and status `422`.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
package/dist/device.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { o as AuthSession } from "./types-
|
|
1
|
+
import { C as AuthErrorStatus, S as AuthErrorCode, _ as SocialIdTokenInput, l as DriveCredential, o as AuthSession, u as DriveCredentialInput, v as SocialIdTokenProvider, x as AuthError, y as UpdateProfileInput } from "./types-B1SZbs6Z.js";
|
|
2
2
|
//#region src/device.d.ts
|
|
3
3
|
type DeviceAuthorizationErrorCode = "device_access_denied" | "device_authorization_cancelled" | "device_code_expired" | "invalid_device_authorization" | "identity_authority_unavailable";
|
|
4
4
|
declare class DeviceAuthorizationError extends Error {
|
|
@@ -43,9 +43,15 @@ interface DeviceAuthClient {
|
|
|
43
43
|
pollForToken(authorization: DeviceAuthorization, options?: PollDeviceOptions): Promise<DeviceToken>;
|
|
44
44
|
getSession(): Promise<AuthSession | null>;
|
|
45
45
|
getToken(): Promise<DeviceToken | null>;
|
|
46
|
+
signUpWithEmail(email: string, password: string, name: string): Promise<AuthSession>;
|
|
47
|
+
signInWithEmail(email: string, password: string): Promise<AuthSession>;
|
|
48
|
+
signInWithSocialIdToken(input: SocialIdTokenInput): Promise<AuthSession>;
|
|
49
|
+
updateProfile(input: UpdateProfileInput): Promise<AuthSession>;
|
|
50
|
+
changePassword(currentPassword: string, newPassword: string, revokeOtherSessions?: boolean): Promise<AuthSession>;
|
|
51
|
+
getDriveCredential(input: DriveCredentialInput): Promise<DriveCredential>;
|
|
46
52
|
revokeCurrentDevice(): Promise<void>;
|
|
47
53
|
revokeAllSessions(): Promise<void>;
|
|
48
54
|
}
|
|
49
55
|
declare function createDeviceAuth(options: DeviceAuthOptions): DeviceAuthClient;
|
|
50
56
|
//#endregion
|
|
51
|
-
export { DeviceAuthClient, DeviceAuthOptions, DeviceAuthorization, DeviceAuthorizationError, DeviceAuthorizationErrorCode, DeviceToken, DeviceTokenPersistence, PollDeviceOptions, createDeviceAuth };
|
|
57
|
+
export { AuthError, type AuthErrorCode, type AuthErrorStatus, DeviceAuthClient, DeviceAuthOptions, DeviceAuthorization, DeviceAuthorizationError, DeviceAuthorizationErrorCode, DeviceToken, DeviceTokenPersistence, PollDeviceOptions, type SocialIdTokenInput, type SocialIdTokenProvider, createDeviceAuth };
|