@synqauth/react 0.0.0-stage → 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aerosol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,47 @@
1
- # Temporary Holding Version
1
+ # @synqauth/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React hooks and components for Synq.
4
+
5
+ Synq is customer identity for businesses: an OpenID Connect issuer for each of your brands, with Solana wallet, social, email and passkey sign-in. The documentation is at [docs.synq.build](https://docs.synq.build).
6
+
7
+ This package is at 0.x and hasn't been published yet: its API can change in any release until Synq's first customer depends on it.
8
+
9
+ ## Quick start
10
+
11
+ `@synqauth/react` talks to your app's own route mount, which `@synqauth/nextjs` serves, so the browser never holds a Synq token. Set that up first (its README), then give the pages that need the session a provider, in a route group's layout so the rest of the app (marketing pages, say) stays static:
12
+
13
+ ```tsx
14
+ // app/(app)/layout.tsx
15
+ import { SynqProvider } from '@synqauth/react'
16
+ import { synq } from '@/lib/synq'
17
+
18
+ export default async function AppLayout({ children }: { children: React.ReactNode }) {
19
+ return <SynqProvider initialSession={await synq.getSession()}>{children}</SynqProvider>
20
+ }
21
+ ```
22
+
23
+ ```tsx
24
+ // app/(app)/settings/page.tsx
25
+ import '@synqauth/react/styles.css'
26
+ import { AccountManager, UserButton, WalletManager } from '@synqauth/react'
27
+
28
+ export default function Settings() {
29
+ return (
30
+ <>
31
+ <UserButton accountHref="/settings" />
32
+ <AccountManager />
33
+ <WalletManager />
34
+ </>
35
+ )
36
+ }
37
+ ```
38
+
39
+ The hooks are `useSession()`, `useUser()`, `useWallets()`, `useLinkedAccounts()`, `useAccountSessions()` and `useAuth()`. Every call answers `{ data, error }` and never throws, and a change clicked twice while it's under way is sent once.
40
+
41
+ The components take Synq's look from `@synqauth/react/styles.css`, themed with CSS variables (`--synq-color-primary`, `--synq-radius`...), or none with `unstyled`. `<SynqProvider messages>` puts their words in your app's language.
42
+
43
+ Plugins hold functions, which a server component can't pass: an app with plugins wraps `SynqProvider` in a client component of its own.
44
+
45
+ ## License
46
+
47
+ MIT
@@ -0,0 +1,344 @@
1
+
2
+ import { ReactNode } from "react";
3
+ import { AccountComponents, SynqError } from "@synqauth/js";
4
+ //#region src/messages.d.ts
5
+ /** Every word the components show, for `<SynqProvider messages>`; any left out stay English. */
6
+ interface SynqMessages {
7
+ readonly signIn: string;
8
+ readonly signOut: string;
9
+ readonly manageAccount: string;
10
+ /** What to call someone with no name, username or email address. */
11
+ readonly account: string;
12
+ readonly loading: string;
13
+ readonly save: string;
14
+ readonly saving: string;
15
+ readonly saved: string;
16
+ readonly cancel: string;
17
+ readonly profile: string;
18
+ readonly name: string;
19
+ readonly username: string;
20
+ readonly waysToSignIn: string;
21
+ readonly remove: string;
22
+ /** The label of the button that stops a way signing them in. */
23
+ readonly removeAccount: (provider: string, subject: string) => string;
24
+ /** What each way to sign in is called, by provider: `email`, `solana`, `google`... */
25
+ readonly providers: Readonly<Record<string, string>>;
26
+ readonly devices: string;
27
+ /** A session whose sign-in time Synq didn't keep. */
28
+ readonly signedIn: string;
29
+ /** The mark on the session this page is signed in with (`current`). */
30
+ readonly thisDevice: string;
31
+ /** The label of the button that signs a device out. */
32
+ readonly signOutDevice: (when: string) => string;
33
+ readonly apps: (count: number) => string;
34
+ readonly wallets: string;
35
+ readonly noWallets: string;
36
+ readonly primary: string;
37
+ readonly rename: string;
38
+ readonly renameWallet: (name: string) => string;
39
+ readonly makePrimary: string;
40
+ readonly makeWalletPrimary: (name: string) => string;
41
+ /** The label of the field naming a wallet. */
42
+ readonly walletName: (address: string) => string;
43
+ }
44
+ /** The components' English words, which an app's own messages override one by one. */
45
+ export declare const DEFAULT_MESSAGES: SynqMessages;
46
+ //#endregion
47
+ //#region src/types.d.ts
48
+ /** A call's outcome: its data, or the `SynqError` it failed with (branch on `error.code`). */
49
+ type SynqResult<T> = {
50
+ readonly data: T;
51
+ readonly error: null;
52
+ } | {
53
+ readonly data: null;
54
+ readonly error: SynqError;
55
+ };
56
+ /** A signed-in person, from the ID token Synq issued at sign-in. */
57
+ interface SynqUser {
58
+ /** The user's id at the brand (`usr_…`). */
59
+ readonly sub: string;
60
+ readonly email?: string;
61
+ readonly email_verified?: boolean;
62
+ readonly name?: string;
63
+ readonly preferred_username?: string;
64
+ readonly picture?: string;
65
+ /** When they signed in, in seconds since the epoch. */
66
+ readonly auth_time?: number;
67
+ /** How they signed in: `pwd`, `otp`, `wallet`, `hwk`, `fed`... */
68
+ readonly amr?: readonly string[];
69
+ readonly [claim: string]: unknown;
70
+ }
71
+ /** Who is signed in, as the app's server tells the browser: `await synq.getSession()`, or the route mount's `session`. */
72
+ interface SynqSessionInfo {
73
+ readonly user: SynqUser | null;
74
+ /** When the session's main access token expires, in seconds since the epoch. */
75
+ readonly expiresAt: number | null;
76
+ }
77
+ /**
78
+ * What the brand keeps for the person (the account API's `metadata`).
79
+ * Augment it to type what the app reads:
80
+ *
81
+ * ```ts
82
+ * declare module '@synqauth/react' {
83
+ * interface UserMetadata {
84
+ * plan?: 'free' | 'pro'
85
+ * }
86
+ * }
87
+ * ```
88
+ */
89
+ interface UserMetadata {
90
+ readonly [field: string]: unknown;
91
+ }
92
+ /**
93
+ * The ways a brand signs people in, as `synq_method` names them: the hosted
94
+ * page goes straight to one. A brand's own connection joins them the same way:
95
+ *
96
+ * ```ts
97
+ * declare module '@synqauth/react' {
98
+ * interface SignInMethods {
99
+ * okta: true
100
+ * }
101
+ * }
102
+ * ```
103
+ */
104
+ interface SignInMethods {
105
+ readonly password: true;
106
+ readonly email: true;
107
+ readonly passkey: true;
108
+ readonly wallet: true;
109
+ readonly google: true;
110
+ readonly microsoft: true;
111
+ readonly discord: true;
112
+ }
113
+ /** One of the brand's sign-in methods. */
114
+ type SignInMethod = Extract<keyof SignInMethods, string>;
115
+ /** How to send the person to sign in. */
116
+ interface SignInOptions {
117
+ /** Where to come back to: a path on the app, the current page by default. */
118
+ readonly returnTo?: string;
119
+ /** Go straight to one sign-in method. */
120
+ readonly method?: SignInMethod;
121
+ /** Fills in their email address. */
122
+ readonly loginHint?: string;
123
+ readonly prompt?: 'login' | 'none' | 'consent' | 'select_account';
124
+ /** Ask them to sign in again when their last sign-in is older than this many seconds. */
125
+ readonly maxAge?: number;
126
+ /** Offer a new passkey once they've signed in again. */
127
+ readonly intent?: 'add-passkey';
128
+ }
129
+ /** The person's profile at the brand. */
130
+ type Profile = AccountComponents['schemas']['Profile'];
131
+ /** Changes to it: only the fields given change, and `null` clears one. */
132
+ type ProfileUpdate = AccountComponents['schemas']['ProfileUpdate'];
133
+ /** A Solana wallet the person signs in with. */
134
+ type Wallet = AccountComponents['schemas']['Wallet'];
135
+ /** A way the person signs in: their email address, a wallet, a Google account... */
136
+ type LinkedAccount = AccountComponents['schemas']['Identity'];
137
+ /** One of the person's sessions at the brand, on some device. */
138
+ type AccountSession = AccountComponents['schemas']['Session'];
139
+ //#endregion
140
+ //#region src/mount.d.ts
141
+ /** Calls the app's route mount: `request('session')`, `request('account/v1/profile', { method: 'PATCH', ... })`. */
142
+ type SynqMountRequest = (path: string, init?: RequestInit) => Promise<Response>;
143
+ /**
144
+ * A browser plugin: actions over its server plugin's routes. The same shape
145
+ * as `@synqauth/nextjs/client`'s `defineSynqClientPlugin`, so its plugins
146
+ * work here as they are.
147
+ */
148
+ interface SynqClientPlugin<Id extends string = string, Actions = unknown> {
149
+ readonly id: Id;
150
+ actions(request: SynqMountRequest): Actions;
151
+ }
152
+ //#endregion
153
+ //#region src/store.d.ts
154
+ /** `error` when the app couldn't be asked who is signed in, and nothing is known yet: the provider asks again, waiting longer each time. */
155
+ type SessionStatus = 'loading' | 'signed-in' | 'signed-out' | 'error';
156
+ //#endregion
157
+ //#region src/provider.d.ts
158
+ /** What `SynqProvider` takes: the server's session, the route mount, the words and the plugins. */
159
+ interface SynqProviderProps {
160
+ /**
161
+ * Who is signed in, from the server: `await synq.getSession()` in a layout.
162
+ * With it the first render needs no request; without it the provider asks
163
+ * the route mount once it mounts.
164
+ */
165
+ readonly initialSession?: SynqSessionInfo | null;
166
+ /** The route mount: `/api/auth` by default. */
167
+ readonly basePath?: string;
168
+ /**
169
+ * Browser plugins (`defineSynqClientPlugin`), their actions on
170
+ * `useAuth().plugins` and `usePlugin()`. Plugins hold functions, so a
171
+ * server component can't pass them: wrap the provider in a client
172
+ * component of the app's own.
173
+ */
174
+ readonly plugins?: readonly SynqClientPlugin[];
175
+ /** How to reach the mount: the browser's `fetch` by default. */
176
+ readonly fetch?: typeof fetch;
177
+ /** How to go to sign-in and sign-out pages: `location.assign` by default. */
178
+ readonly navigate?: (url: string) => void;
179
+ /** The components' words, in the app's language: any left out are English. */
180
+ readonly messages?: Partial<SynqMessages>;
181
+ readonly children?: ReactNode;
182
+ }
183
+ /** Synq for the components under it. Its route mount and `fetch` are read once, when it mounts. */
184
+ export declare function SynqProvider({ initialSession, basePath, plugins, fetch: fetcher, navigate, messages, children }: SynqProviderProps): ReactNode;
185
+ /** A browser plugin's actions, typed by the plugin, which must be one of the provider's `plugins`. */
186
+ export declare function usePlugin<Actions>(plugin: SynqClientPlugin<string, Actions>): Actions;
187
+ //#endregion
188
+ //#region src/hooks.d.ts
189
+ /** What `useSession()` answers: who is signed in, and whether the app said yet. */
190
+ interface UseSession {
191
+ /** `loading` until the app says who is signed in; never with `initialSession`. */
192
+ readonly status: SessionStatus;
193
+ readonly isLoaded: boolean;
194
+ readonly isSignedIn: boolean;
195
+ readonly user: SynqUser | null;
196
+ /** When the session's main access token expires; the app's server refreshes it. */
197
+ readonly expiresAt: number | null;
198
+ /** Why the app couldn't say who is signed in, the last time it was asked. */
199
+ readonly error: SynqError | null;
200
+ /** Asks the app again. */
201
+ readonly refresh: () => Promise<void>;
202
+ }
203
+ /** Who is signed in. It reads the session alone, and asks the account API nothing. */
204
+ export declare function useSession(): UseSession;
205
+ /** What the account hooks (`useWallets`, `useLinkedAccounts`, `useAccountSessions`) answer: the data, the error, and whether it loaded. */
206
+ interface UseAccountData<T> {
207
+ readonly data: T | null;
208
+ readonly error: SynqError | null;
209
+ /** While it is read, or about to be: someone is signed in and nothing has come back yet. */
210
+ readonly isLoading: boolean;
211
+ /** Reads it again. */
212
+ readonly reload: () => Promise<void>;
213
+ }
214
+ /** The person's wallets, the primary one marked. */
215
+ export declare function useWallets(): UseAccountData<readonly Wallet[]>;
216
+ /** The ways the person signs in: their email address, wallets, social accounts. */
217
+ export declare function useLinkedAccounts(): UseAccountData<readonly LinkedAccount[]>;
218
+ /** The person's sessions at the brand, on every device. */
219
+ export declare function useAccountSessions(): UseAccountData<readonly AccountSession[]>;
220
+ /** What `useUser()` answers: the person's claims, profile and metadata, and `update` for the profile. */
221
+ interface UseUser {
222
+ readonly isLoaded: boolean;
223
+ readonly isSignedIn: boolean;
224
+ /** Who they are, from the ID token: there at once. */
225
+ readonly user: SynqUser | null;
226
+ /** Their profile at the brand, read from the account API. */
227
+ readonly profile: Profile | null;
228
+ /** What the brand keeps for them: `UserMetadata`, augmented by the app. */
229
+ readonly metadata: UserMetadata | null;
230
+ readonly isLoading: boolean;
231
+ readonly error: SynqError | null;
232
+ /** Changes their profile: only the fields given, `null` clearing one. */
233
+ readonly update: (changes: ProfileUpdate) => Promise<SynqResult<Profile>>;
234
+ /** Reads the profile and metadata again. */
235
+ readonly reload: () => Promise<void>;
236
+ }
237
+ /** The signed-in person: their ID token's claims at once, then their profile and metadata. */
238
+ export declare function useUser(): UseUser;
239
+ /** What `useAuth()` answers: `signIn`, `signOut`, `link`, and the account's wallets, ways to sign in and sessions. */
240
+ interface UseAuth {
241
+ readonly isLoaded: boolean;
242
+ readonly isSignedIn: boolean;
243
+ readonly userId: string | null;
244
+ /** Sends the browser to sign in, coming back to `returnTo` (this page by default). */
245
+ readonly signIn: (options?: SignInOptions) => void;
246
+ /** Where `signIn` sends the browser: the route mount's `sign-in`. */
247
+ readonly signInUrl: (options?: SignInOptions) => string;
248
+ /** Ends the session in the app and at the brand, then goes where the brand sends the browser. */
249
+ readonly signOut: () => Promise<SynqResult<null>>;
250
+ /**
251
+ * Adds a way to sign in to the person's account: Synq asks them to sign in
252
+ * again, then for `method`, and the browser comes back to `returnTo` (this
253
+ * page by default). Answers only when it can't start: the method isn't
254
+ * offered, or they have as many ways in as one may.
255
+ */
256
+ readonly link: (method: SignInMethod, options?: {
257
+ returnTo?: string;
258
+ }) => Promise<SynqResult<null>>;
259
+ readonly wallets: {
260
+ readonly list: () => Promise<SynqResult<readonly Wallet[]>>;
261
+ /** Names a wallet; `null` takes its name away. */
262
+ readonly rename: (id: string, label: string | null) => Promise<SynqResult<Wallet>>;
263
+ /** Makes it their primary wallet, in place of the one that was. */
264
+ readonly setPrimary: (id: string) => Promise<SynqResult<Wallet>>;
265
+ };
266
+ /** The ways they sign in. */
267
+ readonly accounts: {
268
+ readonly list: () => Promise<SynqResult<readonly LinkedAccount[]>>;
269
+ /** Stops a way signing them in. */
270
+ readonly unlink: (id: string) => Promise<SynqResult<null>>;
271
+ };
272
+ readonly sessions: {
273
+ readonly list: () => Promise<SynqResult<readonly AccountSession[]>>;
274
+ /** Signs that device out. */
275
+ readonly end: (id: string) => Promise<SynqResult<null>>;
276
+ };
277
+ /** Each browser plugin's actions, by its id (`usePlugin` types them). */
278
+ readonly plugins: Readonly<Record<string, unknown>>;
279
+ }
280
+ /** Everything the signed-in person can do. */
281
+ export declare function useAuth(): UseAuth;
282
+ //#endregion
283
+ //#region src/components/SignedIn.d.ts
284
+ /** Shows its children only while someone is signed in. It renders no element of its own. */
285
+ export declare function SignedIn({ children }: {
286
+ readonly children?: ReactNode;
287
+ }): ReactNode;
288
+ //#endregion
289
+ //#region src/components/SignedOut.d.ts
290
+ /** Shows its children only once the app has said nobody is signed in. It renders no element of its own. */
291
+ export declare function SignedOut({ children }: {
292
+ readonly children?: ReactNode;
293
+ }): ReactNode;
294
+ //#endregion
295
+ //#region src/components/styling.d.ts
296
+ /** What every component takes for its look: Synq's classes, or `unstyled`. */
297
+ interface SynqStyleProps {
298
+ /** The same elements, roles and labels, with none of Synq's classes: the app styles them. */
299
+ readonly unstyled?: boolean;
300
+ /** Added to the component's outer element. */
301
+ readonly className?: string;
302
+ }
303
+ //#endregion
304
+ //#region src/components/SignInButton.d.ts
305
+ /** What `SignInButton` takes: the sign-in's options, its look and its words. */
306
+ interface SignInButtonProps extends SignInOptions, SynqStyleProps {
307
+ /** Its label: the provider's `messages.signIn` by default. */
308
+ readonly children?: ReactNode;
309
+ }
310
+ /** A button that sends the browser to sign in, through the app's route mount. */
311
+ export declare function SignInButton({ unstyled, className, children, ...options }: SignInButtonProps): ReactNode;
312
+ //#endregion
313
+ //#region src/components/UserButton.d.ts
314
+ /** What `UserButton` takes: where its account link goes, and its look. */
315
+ interface UserButtonProps extends SynqStyleProps {
316
+ /** The app's page with `<AccountManager>`, linked as "Manage account". */
317
+ readonly accountHref?: string;
318
+ }
319
+ /**
320
+ * The signed-in person's picture and name, showing a panel to manage their
321
+ * account or sign out: a disclosure (a button that shows and hides it), so
322
+ * its link and button are reached with Tab as anywhere else. Escape or a
323
+ * click elsewhere hides it. Nothing while nobody is signed in.
324
+ */
325
+ export declare function UserButton({ unstyled, className, accountHref }: UserButtonProps): ReactNode;
326
+ //#endregion
327
+ //#region src/components/AccountManager.d.ts
328
+ /** One part of `AccountManager`: the profile, the ways to sign in, or the sessions. */
329
+ type AccountSection = 'profile' | 'accounts' | 'sessions';
330
+ /** What `AccountManager` takes: which sections it shows, and its look. */
331
+ interface AccountManagerProps extends SynqStyleProps {
332
+ /** The parts it shows, in this order: all three by default. */
333
+ readonly sections?: readonly AccountSection[];
334
+ }
335
+ /** Manage the signed-in person's account. Nothing while nobody is signed in. */
336
+ export declare function AccountManager({ unstyled, className, sections }: AccountManagerProps): ReactNode;
337
+ //#endregion
338
+ //#region src/components/WalletManager.d.ts
339
+ /** What `WalletManager` takes: its look. */
340
+ type WalletManagerProps = SynqStyleProps;
341
+ /** Manage the signed-in person's wallets. Nothing while nobody is signed in. */
342
+ export declare function WalletManager({ unstyled, className }: WalletManagerProps): ReactNode;
343
+ //#endregion
344
+ export type { AccountManagerProps, AccountSection, AccountSession, LinkedAccount, Profile, ProfileUpdate, SessionStatus, SignInButtonProps, SignInMethod, SignInMethods, SignInOptions, SynqClientPlugin, SynqMessages, SynqMountRequest, SynqProviderProps, SynqResult, SynqSessionInfo, SynqStyleProps, SynqUser, UseAccountData, UseAuth, UseSession, UseUser, UserButtonProps, UserMetadata, Wallet, WalletManagerProps };