@synqauth/react 0.0.0-stage → 0.2.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 +21 -0
- package/README.md +48 -2
- package/dist/index.d.ts +575 -0
- package/dist/index.js +2180 -0
- package/dist/styles.css +268 -0
- package/package.json +53 -3
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,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @synqauth/react
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
To sign people in on your own page (own-UI sign-in, on for the app in the dashboard), render `<SignInForm returnTo="/dashboard" />`, lay out its parts (`PasskeyButton`, `PasswordForm`, `EmailCodeForm`, `VerifyEmailForm`) your own way, or build your own form on `useSignIn()`. It offers a passkey where the browser can use the brand's on your page (Chrome and Edge 128+, Safari 18+, Firefox 152+), then an emailed code or a password. Anything only Synq's hosted page can do, such as a token gate, goes there.
|
|
40
|
+
|
|
41
|
+
The hooks are `useSession()`, `useSignIn()`, `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.
|
|
42
|
+
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
Plugins hold functions, which a server component can't pass: an app with plugins wraps `SynqProvider` in a client component of its own.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
|
|
2
|
+
import { InputHTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { AccountComponents, ChallengeMethod, ChallengeProblem, DeniedReason, RedirectReason, SynqError } from "@synqauth/js";
|
|
4
|
+
//#region src/sign-in.d.ts
|
|
5
|
+
/** Where a sign-in is: what the form shows. */
|
|
6
|
+
type SignInStep = 'start' | 'code' | 'verify-email' | 'done';
|
|
7
|
+
/** What went wrong with the last step: branch on `code`, never on the English. */
|
|
8
|
+
interface SignInProblem {
|
|
9
|
+
/**
|
|
10
|
+
* Synq's problem with the step (`wrong_credentials`, `wrong_code`…), or
|
|
11
|
+
* the sign-in's own: `expired` (it timed out: start again), `refused`
|
|
12
|
+
* (this person can't sign in here: see `reason`), `passkey_cancelled`
|
|
13
|
+
* (the browser's passkey prompt closed with none), `unavailable` (the
|
|
14
|
+
* app's server couldn't be reached, or failed).
|
|
15
|
+
*/
|
|
16
|
+
readonly code: ChallengeProblem | 'expired' | 'refused' | 'passkey_cancelled' | 'unavailable';
|
|
17
|
+
/** Why, when `refused`. */
|
|
18
|
+
readonly reason?: DeniedReason;
|
|
19
|
+
/** Synq's words for it, in English: show your own for `code` in another language. */
|
|
20
|
+
readonly message: string;
|
|
21
|
+
/** Seconds to wait first, when Synq said. */
|
|
22
|
+
readonly retryAfter?: number;
|
|
23
|
+
}
|
|
24
|
+
/** What only the hosted page can do for this sign-in, and what to take there. */
|
|
25
|
+
interface SignInHandover {
|
|
26
|
+
readonly reason: RedirectReason;
|
|
27
|
+
readonly method?: ChallengeMethod;
|
|
28
|
+
readonly loginHint?: string;
|
|
29
|
+
}
|
|
30
|
+
/** What `useSignIn()` takes: where to go once signed in, or what to do instead. */
|
|
31
|
+
interface UseSignInOptions {
|
|
32
|
+
/** Where to go once signed in: the page it started on by default. */
|
|
33
|
+
readonly returnTo?: string;
|
|
34
|
+
/** Called once signed in, with where to go, instead of going there. */
|
|
35
|
+
readonly onSignedIn?: (returnTo: string) => void;
|
|
36
|
+
}
|
|
37
|
+
/** What `useSignIn()` answers: the sign-in's step, what went wrong, and each method's steps. */
|
|
38
|
+
interface UseSignIn {
|
|
39
|
+
readonly step: SignInStep;
|
|
40
|
+
/** Whether a step is on its way to Synq. */
|
|
41
|
+
readonly pending: boolean;
|
|
42
|
+
readonly problem: SignInProblem | null;
|
|
43
|
+
/** The address a code went to. */
|
|
44
|
+
readonly email: string | undefined;
|
|
45
|
+
/** Whether a new account's address may be left unverified for now. */
|
|
46
|
+
readonly canSkip: boolean;
|
|
47
|
+
/** Set when only the hosted page can go on: `continueOnHostedPage()` goes there. */
|
|
48
|
+
readonly handover: SignInHandover | null;
|
|
49
|
+
readonly password: {
|
|
50
|
+
readonly signIn: (input: {
|
|
51
|
+
email: string;
|
|
52
|
+
password: string;
|
|
53
|
+
}) => Promise<void>;
|
|
54
|
+
readonly signUp: (input: {
|
|
55
|
+
email: string;
|
|
56
|
+
password: string;
|
|
57
|
+
}) => Promise<void>;
|
|
58
|
+
};
|
|
59
|
+
readonly emailCode: {
|
|
60
|
+
readonly send: (email: string) => Promise<void>;
|
|
61
|
+
readonly verify: (code: string) => Promise<void>;
|
|
62
|
+
readonly resend: () => Promise<void>;
|
|
63
|
+
};
|
|
64
|
+
readonly passkey: {
|
|
65
|
+
/** Whether this browser takes the issuer's passkeys on this page (related origins). */
|
|
66
|
+
readonly supported: boolean;
|
|
67
|
+
/** Whether it offers them in the address field as the person types (conditional UI). */
|
|
68
|
+
readonly autofill: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* On a brand's custom domain, the brand's own Synq host, whose passkeys
|
|
71
|
+
* work too (`signIn({ related: true })`): known after the first passkey
|
|
72
|
+
* prompt.
|
|
73
|
+
*/
|
|
74
|
+
readonly relatedDomain: string | null;
|
|
75
|
+
readonly signIn: (options?: {
|
|
76
|
+
related?: boolean;
|
|
77
|
+
}) => Promise<void>;
|
|
78
|
+
/** Offers passkeys in the address field (`autocomplete="username webauthn"`): call once it's on the page. */
|
|
79
|
+
readonly startAutofill: () => Promise<void>;
|
|
80
|
+
};
|
|
81
|
+
readonly verifyEmail: {
|
|
82
|
+
readonly verify: (code: string) => Promise<void>;
|
|
83
|
+
readonly resend: () => Promise<void>;
|
|
84
|
+
readonly skip: () => Promise<void>;
|
|
85
|
+
};
|
|
86
|
+
/** Goes on at the hosted page, with the method and address Synq named, or these. */
|
|
87
|
+
readonly continueOnHostedPage: (options?: {
|
|
88
|
+
method?: ChallengeMethod;
|
|
89
|
+
loginHint?: string;
|
|
90
|
+
}) => void;
|
|
91
|
+
/** Drops the sign-in under way and goes back to the first step. */
|
|
92
|
+
readonly startOver: () => Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
/** One sign-in on the app's own screens, through the route mount's `sign-in/flow`. */
|
|
95
|
+
export declare function useSignIn(options?: UseSignInOptions): UseSignIn;
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/messages.d.ts
|
|
98
|
+
/** Every word the components show, for `<SynqProvider messages>`; any left out stay English. */
|
|
99
|
+
interface SynqMessages {
|
|
100
|
+
readonly signIn: string;
|
|
101
|
+
readonly signOut: string;
|
|
102
|
+
readonly manageAccount: string;
|
|
103
|
+
/** What to call someone with no name, username or email address. */
|
|
104
|
+
readonly account: string;
|
|
105
|
+
readonly loading: string;
|
|
106
|
+
readonly save: string;
|
|
107
|
+
readonly saving: string;
|
|
108
|
+
readonly saved: string;
|
|
109
|
+
readonly cancel: string;
|
|
110
|
+
readonly profile: string;
|
|
111
|
+
readonly name: string;
|
|
112
|
+
readonly username: string;
|
|
113
|
+
readonly waysToSignIn: string;
|
|
114
|
+
readonly remove: string;
|
|
115
|
+
/** The label of the button that stops a way signing them in. */
|
|
116
|
+
readonly removeAccount: (provider: string, subject: string) => string;
|
|
117
|
+
/** What each way to sign in is called, by provider: `email`, `solana`, `google`... */
|
|
118
|
+
readonly providers: Readonly<Record<string, string>>;
|
|
119
|
+
readonly devices: string;
|
|
120
|
+
/** A session whose sign-in time Synq didn't keep. */
|
|
121
|
+
readonly signedIn: string;
|
|
122
|
+
/** The mark on the session this page is signed in with (`current`). */
|
|
123
|
+
readonly thisDevice: string;
|
|
124
|
+
/** The label of the button that signs a device out. */
|
|
125
|
+
readonly signOutDevice: (when: string) => string;
|
|
126
|
+
readonly apps: (count: number) => string;
|
|
127
|
+
readonly wallets: string;
|
|
128
|
+
readonly noWallets: string;
|
|
129
|
+
readonly primary: string;
|
|
130
|
+
readonly rename: string;
|
|
131
|
+
readonly renameWallet: (name: string) => string;
|
|
132
|
+
readonly makePrimary: string;
|
|
133
|
+
readonly makeWalletPrimary: (name: string) => string;
|
|
134
|
+
/** The label of the field naming a wallet. */
|
|
135
|
+
readonly walletName: (address: string) => string;
|
|
136
|
+
/** API keys for the brand's APIs (AERO-1693). */
|
|
137
|
+
readonly apiKeys: string;
|
|
138
|
+
readonly noApiKeys: string;
|
|
139
|
+
readonly newApiKey: string;
|
|
140
|
+
/** The field picking which of the brand's APIs a key is for. */
|
|
141
|
+
readonly api: string;
|
|
142
|
+
/** The scopes a key may use. */
|
|
143
|
+
readonly keyScopes: string;
|
|
144
|
+
readonly expires: string;
|
|
145
|
+
readonly never: string;
|
|
146
|
+
readonly inDays: (days: number) => string;
|
|
147
|
+
readonly makeKey: string;
|
|
148
|
+
readonly makingKey: string;
|
|
149
|
+
readonly revoke: string;
|
|
150
|
+
/** The label of the button that revokes a key. */
|
|
151
|
+
readonly revokeKey: (name: string) => string;
|
|
152
|
+
readonly revoked: string;
|
|
153
|
+
readonly expired: string;
|
|
154
|
+
readonly lastUsed: (when: string) => string;
|
|
155
|
+
readonly neverUsed: string;
|
|
156
|
+
readonly expiresOn: (when: string) => string;
|
|
157
|
+
/** Over the key just made, the only time it shows. */
|
|
158
|
+
readonly copyKeyNow: string;
|
|
159
|
+
readonly copy: string;
|
|
160
|
+
readonly copied: string;
|
|
161
|
+
readonly done: string;
|
|
162
|
+
/** When making a key needs a sign-in from the last few minutes. */
|
|
163
|
+
readonly keyNeedsFreshSignIn: string;
|
|
164
|
+
readonly signInAgain: string;
|
|
165
|
+
/** `<SignInForm>` (AERO-1411): sign-in on the app's own screens. */
|
|
166
|
+
readonly email: string;
|
|
167
|
+
readonly password: string;
|
|
168
|
+
readonly signInWithPasskey: string;
|
|
169
|
+
/** The button for a passkey made at the brand's own Synq host, on its custom domain. */
|
|
170
|
+
readonly passkeyFor: (domain: string) => string;
|
|
171
|
+
readonly emailMeACode: string;
|
|
172
|
+
/** Between a passkey and the other ways. */
|
|
173
|
+
readonly or: string;
|
|
174
|
+
readonly code: string;
|
|
175
|
+
readonly codeSentTo: (email: string) => string;
|
|
176
|
+
/** Over the code that proves a new account's address. */
|
|
177
|
+
readonly verifyYourEmail: (email: string) => string;
|
|
178
|
+
readonly verify: string;
|
|
179
|
+
readonly sendAnotherCode: string;
|
|
180
|
+
readonly useAnotherWay: string;
|
|
181
|
+
readonly skipForNow: string;
|
|
182
|
+
/** The link to the hosted page's password step, where resetting one lives. */
|
|
183
|
+
readonly forgotPassword: string;
|
|
184
|
+
readonly createAccount: string;
|
|
185
|
+
/** Before the link to the sign-up screen. */
|
|
186
|
+
readonly newHere: string;
|
|
187
|
+
/** Before the link back to the sign-in screen. */
|
|
188
|
+
readonly haveAnAccount: string;
|
|
189
|
+
/** While the browser goes to the hosted page for what only it can do. */
|
|
190
|
+
readonly continuingToSignIn: string;
|
|
191
|
+
readonly tryAgainIn: (seconds: number) => string;
|
|
192
|
+
/** What went wrong with a step, by `SignInProblem.code`; Synq's English for any left out. */
|
|
193
|
+
readonly signInProblems: Readonly<Partial<Record<SignInProblem['code'], string>>>;
|
|
194
|
+
/** Why this person can't sign in here, by `SignInProblem.reason`. */
|
|
195
|
+
readonly signInRefusals: Readonly<Partial<Record<DeniedReason, string>>>;
|
|
196
|
+
}
|
|
197
|
+
/** The components' English words, which an app's own messages override one by one. */
|
|
198
|
+
export declare const DEFAULT_MESSAGES: SynqMessages;
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/types.d.ts
|
|
201
|
+
/** A call's outcome: its data, or the `SynqError` it failed with (branch on `error.code`). */
|
|
202
|
+
type SynqResult<T> = {
|
|
203
|
+
readonly data: T;
|
|
204
|
+
readonly error: null;
|
|
205
|
+
} | {
|
|
206
|
+
readonly data: null;
|
|
207
|
+
readonly error: SynqError;
|
|
208
|
+
};
|
|
209
|
+
/** A signed-in person, from the ID token Synq issued at sign-in. */
|
|
210
|
+
interface SynqUser {
|
|
211
|
+
/** The user's id at the brand (`usr_…`). */
|
|
212
|
+
readonly sub: string;
|
|
213
|
+
readonly email?: string;
|
|
214
|
+
readonly email_verified?: boolean;
|
|
215
|
+
readonly name?: string;
|
|
216
|
+
readonly preferred_username?: string;
|
|
217
|
+
readonly picture?: string;
|
|
218
|
+
/** When they signed in, in seconds since the epoch. */
|
|
219
|
+
readonly auth_time?: number;
|
|
220
|
+
/** How they signed in: `pwd`, `otp`, `wallet`, `hwk`, `fed`... */
|
|
221
|
+
readonly amr?: readonly string[];
|
|
222
|
+
readonly [claim: string]: unknown;
|
|
223
|
+
}
|
|
224
|
+
/** Who is signed in, as the app's server tells the browser: `await synq.getSession()`, or the route mount's `session`. */
|
|
225
|
+
interface SynqSessionInfo {
|
|
226
|
+
readonly user: SynqUser | null;
|
|
227
|
+
/** When the session's main access token expires, in seconds since the epoch. */
|
|
228
|
+
readonly expiresAt: number | null;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* What the brand keeps for the person (the account API's `metadata`).
|
|
232
|
+
* Augment it to type what the app reads:
|
|
233
|
+
*
|
|
234
|
+
* ```ts
|
|
235
|
+
* declare module '@synqauth/react' {
|
|
236
|
+
* interface UserMetadata {
|
|
237
|
+
* plan?: 'free' | 'pro'
|
|
238
|
+
* }
|
|
239
|
+
* }
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
interface UserMetadata {
|
|
243
|
+
readonly [field: string]: unknown;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The ways a brand signs people in, as `synq_method` names them: the hosted
|
|
247
|
+
* page goes straight to one. A brand's own connection joins them the same way:
|
|
248
|
+
*
|
|
249
|
+
* ```ts
|
|
250
|
+
* declare module '@synqauth/react' {
|
|
251
|
+
* interface SignInMethods {
|
|
252
|
+
* okta: true
|
|
253
|
+
* }
|
|
254
|
+
* }
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
interface SignInMethods {
|
|
258
|
+
readonly password: true;
|
|
259
|
+
readonly email: true;
|
|
260
|
+
readonly passkey: true;
|
|
261
|
+
readonly wallet: true;
|
|
262
|
+
readonly google: true;
|
|
263
|
+
readonly microsoft: true;
|
|
264
|
+
readonly discord: true;
|
|
265
|
+
}
|
|
266
|
+
/** One of the brand's sign-in methods. */
|
|
267
|
+
type SignInMethod = Extract<keyof SignInMethods, string>;
|
|
268
|
+
/** How to send the person to sign in. */
|
|
269
|
+
interface SignInOptions {
|
|
270
|
+
/** Where to come back to: a path on the app, the current page by default. */
|
|
271
|
+
readonly returnTo?: string;
|
|
272
|
+
/** Go straight to one sign-in method. */
|
|
273
|
+
readonly method?: SignInMethod;
|
|
274
|
+
/** Fills in their email address. */
|
|
275
|
+
readonly loginHint?: string;
|
|
276
|
+
readonly prompt?: 'login' | 'none' | 'consent' | 'select_account';
|
|
277
|
+
/** Ask them to sign in again when their last sign-in is older than this many seconds. */
|
|
278
|
+
readonly maxAge?: number;
|
|
279
|
+
/** Offer a new passkey once they've signed in again. */
|
|
280
|
+
readonly intent?: 'add-passkey';
|
|
281
|
+
}
|
|
282
|
+
/** The person's profile at the brand. */
|
|
283
|
+
type Profile = AccountComponents['schemas']['Profile'];
|
|
284
|
+
/** Changes to it: only the fields given change, and `null` clears one. */
|
|
285
|
+
type ProfileUpdate = AccountComponents['schemas']['ProfileUpdate'];
|
|
286
|
+
/** A Solana wallet the person signs in with. */
|
|
287
|
+
type Wallet = AccountComponents['schemas']['Wallet'];
|
|
288
|
+
/** A way the person signs in: their email address, a wallet, a Google account... */
|
|
289
|
+
type LinkedAccount = AccountComponents['schemas']['Identity'];
|
|
290
|
+
/** One of the person's sessions at the brand, on some device. */
|
|
291
|
+
type AccountSession = AccountComponents['schemas']['Session'];
|
|
292
|
+
/** One of the person's API keys for the brand's APIs (AERO-1693): never the key itself. */
|
|
293
|
+
type ApiKey = AccountComponents['schemas']['UserApiKey'];
|
|
294
|
+
/** A key just made: `key` is the key itself, in this answer only. */
|
|
295
|
+
type NewApiKey = AccountComponents['schemas']['NewUserApiKey'];
|
|
296
|
+
/** What making an API key takes: the API's identifier, a name, its scopes, an expiry if any. */
|
|
297
|
+
type ApiKeyInput = AccountComponents['schemas']['UserApiKeyCreate'];
|
|
298
|
+
/** One of the brand's APIs the person may make keys for, with the scopes it defines. */
|
|
299
|
+
type KeyableApi = AccountComponents['schemas']['KeyableApi'];
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/mount.d.ts
|
|
302
|
+
/** Calls the app's route mount: `request('session')`, `request('account/v1/profile', { method: 'PATCH', ... })`. */
|
|
303
|
+
type SynqMountRequest = (path: string, init?: RequestInit) => Promise<Response>;
|
|
304
|
+
/**
|
|
305
|
+
* A browser plugin: actions over its server plugin's routes. The same shape
|
|
306
|
+
* as `@synqauth/nextjs/client`'s `defineSynqClientPlugin`, so its plugins
|
|
307
|
+
* work here as they are.
|
|
308
|
+
*/
|
|
309
|
+
interface SynqClientPlugin<Id extends string = string, Actions = unknown> {
|
|
310
|
+
readonly id: Id;
|
|
311
|
+
actions(request: SynqMountRequest): Actions;
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/store.d.ts
|
|
315
|
+
/** `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. */
|
|
316
|
+
type SessionStatus = 'loading' | 'signed-in' | 'signed-out' | 'error';
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/provider.d.ts
|
|
319
|
+
/** What `SynqProvider` takes: the server's session, the route mount, the words and the plugins. */
|
|
320
|
+
interface SynqProviderProps {
|
|
321
|
+
/**
|
|
322
|
+
* Who is signed in, from the server: `await synq.getSession()` in a layout.
|
|
323
|
+
* With it the first render needs no request; without it the provider asks
|
|
324
|
+
* the route mount once it mounts.
|
|
325
|
+
*/
|
|
326
|
+
readonly initialSession?: SynqSessionInfo | null;
|
|
327
|
+
/** The route mount: `/api/auth` by default. */
|
|
328
|
+
readonly basePath?: string;
|
|
329
|
+
/**
|
|
330
|
+
* Browser plugins (`defineSynqClientPlugin`), their actions on
|
|
331
|
+
* `useAuth().plugins` and `usePlugin()`. Plugins hold functions, so a
|
|
332
|
+
* server component can't pass them: wrap the provider in a client
|
|
333
|
+
* component of the app's own.
|
|
334
|
+
*/
|
|
335
|
+
readonly plugins?: readonly SynqClientPlugin[];
|
|
336
|
+
/** How to reach the mount: the browser's `fetch` by default. */
|
|
337
|
+
readonly fetch?: typeof fetch;
|
|
338
|
+
/** How to go to sign-in and sign-out pages: `location.assign` by default. */
|
|
339
|
+
readonly navigate?: (url: string) => void;
|
|
340
|
+
/** The components' words, in the app's language: any left out are English. */
|
|
341
|
+
readonly messages?: Partial<SynqMessages>;
|
|
342
|
+
readonly children?: ReactNode;
|
|
343
|
+
}
|
|
344
|
+
/** Synq for the components under it. Its route mount and `fetch` are read once, when it mounts. */
|
|
345
|
+
export declare function SynqProvider({ initialSession, basePath, plugins, fetch: fetcher, navigate, messages, children }: SynqProviderProps): ReactNode;
|
|
346
|
+
/** A browser plugin's actions, typed by the plugin, which must be one of the provider's `plugins`. */
|
|
347
|
+
export declare function usePlugin<Actions>(plugin: SynqClientPlugin<string, Actions>): Actions;
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region src/hooks.d.ts
|
|
350
|
+
/** What `useSession()` answers: who is signed in, and whether the app said yet. */
|
|
351
|
+
interface UseSession {
|
|
352
|
+
/** `loading` until the app says who is signed in; never with `initialSession`. */
|
|
353
|
+
readonly status: SessionStatus;
|
|
354
|
+
readonly isLoaded: boolean;
|
|
355
|
+
readonly isSignedIn: boolean;
|
|
356
|
+
readonly user: SynqUser | null;
|
|
357
|
+
/** When the session's main access token expires; the app's server refreshes it. */
|
|
358
|
+
readonly expiresAt: number | null;
|
|
359
|
+
/** Why the app couldn't say who is signed in, the last time it was asked. */
|
|
360
|
+
readonly error: SynqError | null;
|
|
361
|
+
/** Asks the app again. */
|
|
362
|
+
readonly refresh: () => Promise<void>;
|
|
363
|
+
}
|
|
364
|
+
/** Who is signed in. It reads the session alone, and asks the account API nothing. */
|
|
365
|
+
export declare function useSession(): UseSession;
|
|
366
|
+
/** What the account hooks (`useWallets`, `useLinkedAccounts`, `useAccountSessions`) answer: the data, the error, and whether it loaded. */
|
|
367
|
+
interface UseAccountData<T> {
|
|
368
|
+
readonly data: T | null;
|
|
369
|
+
readonly error: SynqError | null;
|
|
370
|
+
/** While it is read, or about to be: someone is signed in and nothing has come back yet. */
|
|
371
|
+
readonly isLoading: boolean;
|
|
372
|
+
/** Reads it again. */
|
|
373
|
+
readonly reload: () => Promise<void>;
|
|
374
|
+
}
|
|
375
|
+
/** The person's wallets, the primary one marked. */
|
|
376
|
+
export declare function useWallets(): UseAccountData<readonly Wallet[]>;
|
|
377
|
+
/** The ways the person signs in: their email address, wallets, social accounts. */
|
|
378
|
+
export declare function useLinkedAccounts(): UseAccountData<readonly LinkedAccount[]>;
|
|
379
|
+
/** The person's sessions at the brand, on every device. */
|
|
380
|
+
export declare function useAccountSessions(): UseAccountData<readonly AccountSession[]>;
|
|
381
|
+
/** The person's API keys for the brand's APIs (AERO-1693), revoked and expired ones too. */
|
|
382
|
+
export declare function useApiKeys(): UseAccountData<readonly ApiKey[]>;
|
|
383
|
+
/** The brand's APIs that let the person make keys, with the scopes each defines. */
|
|
384
|
+
export declare function useKeyableApis(): UseAccountData<readonly KeyableApi[]>;
|
|
385
|
+
/** What `useUser()` answers: the person's claims, profile and metadata, and `update` for the profile. */
|
|
386
|
+
interface UseUser {
|
|
387
|
+
readonly isLoaded: boolean;
|
|
388
|
+
readonly isSignedIn: boolean;
|
|
389
|
+
/** Who they are, from the ID token: there at once. */
|
|
390
|
+
readonly user: SynqUser | null;
|
|
391
|
+
/** Their profile at the brand, read from the account API. */
|
|
392
|
+
readonly profile: Profile | null;
|
|
393
|
+
/** What the brand keeps for them: `UserMetadata`, augmented by the app. */
|
|
394
|
+
readonly metadata: UserMetadata | null;
|
|
395
|
+
readonly isLoading: boolean;
|
|
396
|
+
readonly error: SynqError | null;
|
|
397
|
+
/** Changes their profile: only the fields given, `null` clearing one. */
|
|
398
|
+
readonly update: (changes: ProfileUpdate) => Promise<SynqResult<Profile>>;
|
|
399
|
+
/** Reads the profile and metadata again. */
|
|
400
|
+
readonly reload: () => Promise<void>;
|
|
401
|
+
}
|
|
402
|
+
/** The signed-in person: their ID token's claims at once, then their profile and metadata. */
|
|
403
|
+
export declare function useUser(): UseUser;
|
|
404
|
+
/** What `useAuth()` answers: `signIn`, `signOut`, `link`, and the account's wallets, ways to sign in and sessions. */
|
|
405
|
+
interface UseAuth {
|
|
406
|
+
readonly isLoaded: boolean;
|
|
407
|
+
readonly isSignedIn: boolean;
|
|
408
|
+
readonly userId: string | null;
|
|
409
|
+
/** Sends the browser to sign in, coming back to `returnTo` (this page by default). */
|
|
410
|
+
readonly signIn: (options?: SignInOptions) => void;
|
|
411
|
+
/** Where `signIn` sends the browser: the route mount's `sign-in`. */
|
|
412
|
+
readonly signInUrl: (options?: SignInOptions) => string;
|
|
413
|
+
/** Ends the session in the app and at the brand, then goes where the brand sends the browser. */
|
|
414
|
+
readonly signOut: () => Promise<SynqResult<null>>;
|
|
415
|
+
/**
|
|
416
|
+
* Adds a way to sign in to the person's account: Synq asks them to sign in
|
|
417
|
+
* again, then for `method`, and the browser comes back to `returnTo` (this
|
|
418
|
+
* page by default). Answers only when it can't start: the method isn't
|
|
419
|
+
* offered, or they have as many ways in as one may.
|
|
420
|
+
*/
|
|
421
|
+
readonly link: (method: SignInMethod, options?: {
|
|
422
|
+
returnTo?: string;
|
|
423
|
+
}) => Promise<SynqResult<null>>;
|
|
424
|
+
readonly wallets: {
|
|
425
|
+
readonly list: () => Promise<SynqResult<readonly Wallet[]>>;
|
|
426
|
+
/** Names a wallet; `null` takes its name away. */
|
|
427
|
+
readonly rename: (id: string, label: string | null) => Promise<SynqResult<Wallet>>;
|
|
428
|
+
/** Makes it their primary wallet, in place of the one that was. */
|
|
429
|
+
readonly setPrimary: (id: string) => Promise<SynqResult<Wallet>>;
|
|
430
|
+
};
|
|
431
|
+
/** The ways they sign in. */
|
|
432
|
+
readonly accounts: {
|
|
433
|
+
readonly list: () => Promise<SynqResult<readonly LinkedAccount[]>>;
|
|
434
|
+
/** Stops a way signing them in. */
|
|
435
|
+
readonly unlink: (id: string) => Promise<SynqResult<null>>;
|
|
436
|
+
};
|
|
437
|
+
readonly sessions: {
|
|
438
|
+
readonly list: () => Promise<SynqResult<readonly AccountSession[]>>;
|
|
439
|
+
/** Signs that device out. */
|
|
440
|
+
readonly end: (id: string) => Promise<SynqResult<null>>;
|
|
441
|
+
};
|
|
442
|
+
/** Their API keys for the brand's own APIs (AERO-1693). */
|
|
443
|
+
readonly apiKeys: {
|
|
444
|
+
readonly list: () => Promise<SynqResult<readonly ApiKey[]>>;
|
|
445
|
+
/**
|
|
446
|
+
* Makes a key: its `key` is in this answer only, so show it once. Making
|
|
447
|
+
* one needs a sign-in from the last 5 minutes (`fresh_sign_in_required`:
|
|
448
|
+
* `signIn({ maxAge: 300 })`).
|
|
449
|
+
*/
|
|
450
|
+
readonly create: (input: ApiKeyInput) => Promise<SynqResult<NewApiKey>>;
|
|
451
|
+
/** Revokes a key at once. */
|
|
452
|
+
readonly revoke: (id: string) => Promise<SynqResult<null>>;
|
|
453
|
+
};
|
|
454
|
+
/** Each browser plugin's actions, by its id (`usePlugin` types them). */
|
|
455
|
+
readonly plugins: Readonly<Record<string, unknown>>;
|
|
456
|
+
}
|
|
457
|
+
/** Everything the signed-in person can do. */
|
|
458
|
+
export declare function useAuth(): UseAuth;
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src/components/SignedIn.d.ts
|
|
461
|
+
/** Shows its children only while someone is signed in. It renders no element of its own. */
|
|
462
|
+
export declare function SignedIn({ children }: {
|
|
463
|
+
readonly children?: ReactNode;
|
|
464
|
+
}): ReactNode;
|
|
465
|
+
//#endregion
|
|
466
|
+
//#region src/components/SignedOut.d.ts
|
|
467
|
+
/** Shows its children only once the app has said nobody is signed in. It renders no element of its own. */
|
|
468
|
+
export declare function SignedOut({ children }: {
|
|
469
|
+
readonly children?: ReactNode;
|
|
470
|
+
}): ReactNode;
|
|
471
|
+
//#endregion
|
|
472
|
+
//#region src/components/styling.d.ts
|
|
473
|
+
/** What every component takes for its look: Synq's classes, or `unstyled`. */
|
|
474
|
+
interface SynqStyleProps {
|
|
475
|
+
/** The same elements, roles and labels, with none of Synq's classes: the app styles them. */
|
|
476
|
+
readonly unstyled?: boolean;
|
|
477
|
+
/** Added to the component's outer element. */
|
|
478
|
+
readonly className?: string;
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/components/SignInButton.d.ts
|
|
482
|
+
/** What `SignInButton` takes: the sign-in's options, its look and its words. */
|
|
483
|
+
interface SignInButtonProps extends SignInOptions, SynqStyleProps {
|
|
484
|
+
/** Its label: the provider's `messages.signIn` by default. */
|
|
485
|
+
readonly children?: ReactNode;
|
|
486
|
+
}
|
|
487
|
+
/** A button that sends the browser to sign in, through the app's route mount. */
|
|
488
|
+
export declare function SignInButton({ unstyled, className, children, ...options }: SignInButtonProps): ReactNode;
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/components/SignInForm.d.ts
|
|
491
|
+
/** What `SignInForm` takes: which ways to offer, where it starts, and its look. */
|
|
492
|
+
interface SignInFormProps extends UseSignInOptions, SynqStyleProps {
|
|
493
|
+
/** The ways to offer, of those the app has on: all three by default. */
|
|
494
|
+
readonly methods?: readonly ChallengeMethod[];
|
|
495
|
+
/** Which screen it opens on. People can switch, where passwords are on. */
|
|
496
|
+
readonly screen?: 'sign-in' | 'sign-up';
|
|
497
|
+
}
|
|
498
|
+
/** What each part of the form takes: the sign-in it works on, and its look. */
|
|
499
|
+
interface SignInPartProps extends SynqStyleProps {
|
|
500
|
+
/** The sign-in, from `useSignIn()`. */
|
|
501
|
+
readonly signIn: UseSignIn;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Sign-in on the app's own page: a passkey where the browser takes the
|
|
505
|
+
* issuer's, then an emailed code or a password, and the code or the new
|
|
506
|
+
* address's proof after. Whatever only the hosted page can do, it goes
|
|
507
|
+
* there for.
|
|
508
|
+
*/
|
|
509
|
+
export declare function SignInForm({ methods, screen: initialScreen, returnTo, onSignedIn, unstyled, className }: SignInFormProps): ReactNode;
|
|
510
|
+
/** The button for a passkey: the brand's, or with `related` its own host's on its custom domain. */
|
|
511
|
+
export declare function PasskeyButton({ signIn, related, children, unstyled, className }: SignInPartProps & {
|
|
512
|
+
readonly related?: boolean;
|
|
513
|
+
readonly children?: ReactNode;
|
|
514
|
+
}): ReactNode;
|
|
515
|
+
/**
|
|
516
|
+
* An email address and a password: signs in, or with `screen="sign-up"`
|
|
517
|
+
* makes an account. With `emailCode`, it offers a code by email for the
|
|
518
|
+
* same address too.
|
|
519
|
+
*/
|
|
520
|
+
export declare function PasswordForm({ signIn, screen, emailCode, autofill, unstyled, className }: SignInPartProps & {
|
|
521
|
+
readonly screen?: 'sign-in' | 'sign-up';
|
|
522
|
+
readonly emailCode?: boolean;
|
|
523
|
+
/** Offer passkeys in the address field as the person types, where the browser can. */
|
|
524
|
+
readonly autofill?: boolean;
|
|
525
|
+
}): ReactNode;
|
|
526
|
+
/** An emailed code: the address to send it to, then the code it held. */
|
|
527
|
+
export declare function EmailCodeForm({ signIn, autofill, unstyled, className }: SignInPartProps & {
|
|
528
|
+
/** Offer passkeys in the address field as the person types, where the browser can. */
|
|
529
|
+
readonly autofill?: boolean;
|
|
530
|
+
}): ReactNode;
|
|
531
|
+
/** The code that proves a new account's address, or "Skip for now" where the brand allows. */
|
|
532
|
+
export declare function VerifyEmailForm({ signIn, unstyled, className }: SignInPartProps): ReactNode;
|
|
533
|
+
/** What `OneTimeCodeInput` takes: its label, its look, and any input attribute. */
|
|
534
|
+
interface OneTimeCodeInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className'>, SynqStyleProps {
|
|
535
|
+
readonly label: ReactNode;
|
|
536
|
+
}
|
|
537
|
+
/** The field for an emailed code, named `code`: digits, filled from the message where the device can. */
|
|
538
|
+
export declare function OneTimeCodeInput({ label, unstyled, className, ...input }: OneTimeCodeInputProps): ReactNode;
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region src/components/UserButton.d.ts
|
|
541
|
+
/** What `UserButton` takes: where its account link goes, and its look. */
|
|
542
|
+
interface UserButtonProps extends SynqStyleProps {
|
|
543
|
+
/** The app's page with `<AccountManager>`, linked as "Manage account". */
|
|
544
|
+
readonly accountHref?: string;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* The signed-in person's picture and name, showing a panel to manage their
|
|
548
|
+
* account or sign out: a disclosure (a button that shows and hides it), so
|
|
549
|
+
* its link and button are reached with Tab as anywhere else. Escape or a
|
|
550
|
+
* click elsewhere hides it. Nothing while nobody is signed in.
|
|
551
|
+
*/
|
|
552
|
+
export declare function UserButton({ unstyled, className, accountHref }: UserButtonProps): ReactNode;
|
|
553
|
+
//#endregion
|
|
554
|
+
//#region src/components/AccountManager.d.ts
|
|
555
|
+
/** One part of `AccountManager`: the profile, the ways to sign in, the sessions, or the API keys. */
|
|
556
|
+
type AccountSection = 'profile' | 'accounts' | 'sessions' | 'apiKeys';
|
|
557
|
+
/** What `AccountManager` takes: which sections it shows, and its look. */
|
|
558
|
+
interface AccountManagerProps extends SynqStyleProps {
|
|
559
|
+
/**
|
|
560
|
+
* The parts it shows, in this order: all four by default. The API keys
|
|
561
|
+
* show only where one of the brand's APIs lets people make keys, or the
|
|
562
|
+
* person has some.
|
|
563
|
+
*/
|
|
564
|
+
readonly sections?: readonly AccountSection[];
|
|
565
|
+
}
|
|
566
|
+
/** Manage the signed-in person's account. Nothing while nobody is signed in. */
|
|
567
|
+
export declare function AccountManager({ unstyled, className, sections }: AccountManagerProps): ReactNode;
|
|
568
|
+
//#endregion
|
|
569
|
+
//#region src/components/WalletManager.d.ts
|
|
570
|
+
/** What `WalletManager` takes: its look. */
|
|
571
|
+
type WalletManagerProps = SynqStyleProps;
|
|
572
|
+
/** Manage the signed-in person's wallets. Nothing while nobody is signed in. */
|
|
573
|
+
export declare function WalletManager({ unstyled, className }: WalletManagerProps): ReactNode;
|
|
574
|
+
//#endregion
|
|
575
|
+
export type { AccountManagerProps, AccountSection, AccountSession, ApiKey, ApiKeyInput, KeyableApi, LinkedAccount, NewApiKey, OneTimeCodeInputProps, Profile, ProfileUpdate, SessionStatus, SignInButtonProps, SignInFormProps, SignInHandover, SignInMethod, SignInMethods, SignInOptions, SignInPartProps, SignInProblem, SignInStep, SynqClientPlugin, SynqMessages, SynqMountRequest, SynqProviderProps, SynqResult, SynqSessionInfo, SynqStyleProps, SynqUser, UseAccountData, UseAuth, UseSession, UseSignIn, UseSignInOptions, UseUser, UserButtonProps, UserMetadata, Wallet, WalletManagerProps };
|