@withone/connect 0.15.0 → 0.16.1

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: one-connect
3
- description: Add One Connect to an application so its users can grant the app scoped, revocable access to their own One-connected tools (Gmail, Slack, Notion, Stripe and 500 more). Use when wiring @withone/connect into an app - the button, the two backend routes, the connect key, and calling One with the grant.
3
+ description: Add One Connect to an application so its users can grant the app scoped, revocable access to their own One-connected tools (Gmail, Slack, Notion, Stripe and 900+ more). Use when wiring @withone/connect into an app - choosing key or token mode, the two backend routes, the Connect button or the app's own button, and calling One with the grant.
4
4
  ---
5
5
 
6
6
  # One Connect
@@ -13,19 +13,31 @@ you wire three things: a button, two routes, and the calls made with the grant.
13
13
  Browser Your backend One
14
14
  <ConnectButton> ------> GET /api/one/authorize ---302---> hosted page: sign in, pick tools, set access
15
15
  GET /api/one/callback <--302---- ?code&state
16
- saves one id for the user, redirects home
16
+ saves the grant for the user, redirects home
17
17
  <------ onSuccess fires
18
18
  Later: oneConnect.runAction(userId, ...) -> One, grant enforced
19
19
  ```
20
20
 
21
- Use **key mode**, described in sections 1 to 7: the app holds one connect
22
- key and saves one id per user, and nothing is refreshed. Token mode
23
- (section 9) is the other way to hold the grant; use it only when the human
24
- asks for it, or when the app already passes a `tokenStore`.
25
-
26
21
  The client secret and the connect key stay on the server.
27
22
 
28
- ## 1 - Ask the human for these
23
+ ## 1 - Choose the mode
24
+
25
+ The routes, the button and the calls are the same in both modes. Only what
26
+ the server keeps differs.
27
+
28
+ | | Key mode (default) | Token mode |
29
+ |---|---|---|
30
+ | Server keeps | one connect key for the app, one id per user | an access and a refresh token per user |
31
+ | Expires | nothing | yes; the app runs a daily refresh job |
32
+ | User revoked | next call throws `reconnect_required` | next refresh throws `refresh_failed` |
33
+ | Call outside the grant | `403`, `blockedByGrant: true` | `403`, `blockedByGrant` stays `false` |
34
+
35
+ Use **key mode** unless the human asks for token mode, the app needs the
36
+ bearer token itself, or the app already passes a `tokenStore`. Do not switch
37
+ an existing app from one mode to the other unless asked. Sections 2 to 9 are
38
+ key mode; section 10 lists what changes for token mode.
39
+
40
+ ## 2 - Ask the human for these
29
41
 
30
42
  They create the app in the One dashboard: Developers -> Connect -> New app.
31
43
 
@@ -33,14 +45,14 @@ They create the app in the One dashboard: Developers -> Connect -> New app.
33
45
  |---|---|
34
46
  | `ONE_CLIENT_ID` | From the app. |
35
47
  | `ONE_CLIENT_SECRET` | Starts with `one_secret_`. Shown once. |
36
- | `ONE_CONNECT_KEY` | On the app's page: Credentials -> Connect keys -> Create key, with the dashboard on Production. Shown once. |
48
+ | `ONE_CONNECT_KEY` | Key mode. On the app's page: Credentials -> Connect keys -> Create key, with the dashboard on Production. Shown once. |
37
49
  | Redirect URI | Registered on the app. Must match the callback route exactly, e.g. `http://localhost:3000/api/one/callback`. |
38
- | `ONE_PERMISSION_SET` | Optional. The tools and access levels to ask for. |
50
+ | `ONE_PERMISSION_SET` | Optional. The app's Permission set ID: the tools and access levels to ask for. Without it, the consent page lists the user's connections and they choose. |
39
51
 
40
52
  Never ask the human to paste the secret or the connect key into the chat.
41
53
  Tell them which environment variable to set and read it from there.
42
54
 
43
- ## 2 - Environment (server only)
55
+ ## 3 - Environment (server only)
44
56
 
45
57
  ```bash
46
58
  ONE_CLIENT_ID=...
@@ -50,7 +62,7 @@ ONE_REDIRECT_URI=https://yourapp.com/api/one/callback
50
62
  ONE_PERMISSION_SET=... # optional
51
63
  ```
52
64
 
53
- ## 3 - Install and create the client
65
+ ## 4 - Install and create the client
54
66
 
55
67
  ```bash
56
68
  npm install @withone/connect
@@ -67,9 +79,9 @@ export const oneConnect = createOneConnect({
67
79
  permissionSet: process.env.ONE_PERMISSION_SET,
68
80
  connectKey: process.env.ONE_CONNECT_KEY!,
69
81
  userStore: {
70
- saveUser: (userId, reference) => /* save the string on the app's user row */,
71
- loadUser: (userId) => /* read it; null when never connected */,
72
- clearUser: (userId) => /* set it to null */,
82
+ saveUser: async (userId, reference) => { /* save the string on the app's user row */ },
83
+ loadUser: async (userId) => /* read it; null when never connected */,
84
+ clearUser: async (userId) => { /* set it to null */ },
73
85
  },
74
86
  });
75
87
  ```
@@ -83,7 +95,10 @@ identifier, not a secret, so it needs no encryption and no lock. The
83
95
  package writes it in the callback and reads it on every call; the app
84
96
  never passes it anywhere.
85
97
 
86
- ## 4 - The two routes
98
+ Optional settings: `returnTo` (where the callback sends the browser, `/` by
99
+ default) and `oneApiUrl` (One's API origin, production by default).
100
+
101
+ ## 5 - The two routes
87
102
 
88
103
  Next.js App Router (also Remix, SvelteKit, Hono, Bun):
89
104
 
@@ -94,16 +109,31 @@ import { oneConnect } from "@/lib/one";
94
109
 
95
110
  export const { GET } = createOneConnectRoutes(oneConnect, {
96
111
  identifyUser: async (request) => /* the signed-in user's id, or null */,
112
+ signInUrl: "/login", // where signed-out users go; 401 when omitted
97
113
  loginHintFor: async (request) => /* their email, optional */,
98
- signInUrl: "/login",
114
+ onComplete: ({ userId, result }) => { /* log result.outcome and, on failure, result.message */ },
99
115
  });
100
116
  ```
101
117
 
102
- Express or plain Node: `createOneConnectHandlers(oneConnect, { identifyUser })`
103
- from `@withone/connect/node`, mounted at `/api/one/authorize` and
104
- `/api/one/callback`.
118
+ `identifyUser` returns the id of the app's own signed-in user, from the
119
+ app's existing session. It is not the email: One's page signs the person
120
+ in to One with their email and a code; this id is which of the app's
121
+ accounts the grant is saved under, and the `userId` every later call
122
+ takes. `loginHintFor` only pre-fills that email. If the app has no
123
+ sign-in, ask the human what to use; for a throwaway demo, one fixed id
124
+ is fine.
125
+
126
+ Express, Fastify, Koa or plain Node: `createOneConnectHandlers(oneConnect,
127
+ options)` from `@withone/connect/node` takes the same options and returns
128
+ `{ authorize, callback }`, mounted at `/api/one/authorize` and
129
+ `/api/one/callback`. Both routes must share that directory: the state
130
+ cookie is scoped to it.
131
+
132
+ ## 6 - The button
105
133
 
106
- ## 5 - The button
134
+ Pick by what the app already has. Each option uses the routes above.
135
+
136
+ **The ready-made button** (default):
107
137
 
108
138
  ```tsx
109
139
  import { ConnectButton } from "@withone/connect/react";
@@ -118,20 +148,48 @@ import { ConnectButton } from "@withone/connect/react";
118
148
  ```
119
149
 
120
150
  Optional props: `variant` ("default" | "accent" | "block"), `size` ("sm" |
121
- "md" | "lg"), `fullWidth`, `theme` ("light" | "dark" | "auto"), `label`,
122
- `description`, `disabled`. The accent variant's colours come from the host's
123
- `--one-connect-accent` and `--one-connect-accent-fg` CSS variables.
151
+ "md" | "lg"), `fullWidth`, `theme` ("light" | "dark" | "auto"),
152
+ `connectTheme` ("light" | "dark", One's page), `label`, `connectedLabel`,
153
+ `description`, `disabled`, `onCancel`. The accent variant's colours come
154
+ from the host's `--one-connect-accent` and `--one-connect-accent-fg` CSS
155
+ variables.
124
156
 
125
157
  Vue: `@withone/connect/vue`, same props. Svelte: `use:connectButton` from
126
158
  `@withone/connect/svelte`. Anything else: `import "@withone/connect"` and use
127
159
  `<one-connect-button authorize-url="/api/one/authorize" logos="gmail, stripe">`.
128
- A custom button in React: `useOneConnect({ authorizeUrl })` returns `{ open, status }`.
129
160
 
130
- ## 6 - Calling One with the grant
161
+ **The app's own button.** When the app has its own design system button,
162
+ keep it and wire the flow to it instead of adding a second style:
163
+
164
+ - React: `useOneConnect` from `@withone/connect/react`.
131
165
 
132
- The same four steps the One CLI takes: find the action, read its knowledge,
133
- run it. Always read the knowledge before running an action for the first
134
- time; it names the required fields, the encoding and any header.
166
+ ```tsx
167
+ const { open, status, error } = useOneConnect({ authorizeUrl: "/api/one/authorize", onSuccess: refetch });
168
+ <Button onClick={open} disabled={status === "connecting"}>Connect your tools</Button>
169
+ ```
170
+
171
+ - Any other framework, or none: `createConnectFlow` from `@withone/connect`.
172
+ Create it once where the button lives (Vue `onMounted`, Svelte `onMount`),
173
+ call `flow.open()` on click, and `flow.destroy()` when the button goes.
174
+
175
+ ```ts
176
+ const flow = createConnectFlow({ authorizeUrl: "/api/one/authorize", onSuccess, onError, onCancel });
177
+ button.addEventListener("click", () => flow.open());
178
+ ```
179
+
180
+ - No SDK in the browser (server-rendered pages): a plain link,
181
+ `<a href="/api/one/authorize">`. The user returns with
182
+ `?one_connect=success`, or `?one_connect=error&one_connect_error=` with
183
+ `declined`, `expired` or `failed`. Show the app's own text per code, never
184
+ text from the URL, and remove the parameters after reading them.
185
+
186
+ Do not build a completion page; the callback redirect is the completion.
187
+
188
+ ## 7 - Calling One with the grant
189
+
190
+ The same four steps the One CLI takes: list, find the action, read its
191
+ knowledge, run it. Always read the knowledge before running an action for
192
+ the first time; it names the required fields, the encoding and any header.
135
193
 
136
194
  ```ts
137
195
  const connections = await oneConnect.listConnections(userId); // [{ key, platform, access }]
@@ -150,14 +208,19 @@ const reply = await oneConnect.runAction(userId, {
150
208
  // { status, ok, blockedByGrant, data }
151
209
  ```
152
210
 
211
+ Each connection's `access` is `{ policy: "full" }`, `{ policy: "methods",
212
+ methods }` or `{ policy: "actions", actions }`; plan from it before calling.
153
213
  `runAction` takes the method and path from the action, fills the path's
154
214
  placeholders, puts the connection key in the body of an action One serves
155
215
  itself (tag `custom`), and encodes the body as asked. `listActions(userId,
156
- platform)` lists a whole catalog when search is not enough.
216
+ platform)` lists a whole catalog when search is not enough;
217
+ `oneConnect.fetch(userId, path, init)` reaches any other `/v1` endpoint.
157
218
 
158
219
  Do not set any auth header. The package adds the connect key and the
159
220
  user's id to every call it makes.
160
221
 
222
+ ## 8 - Errors
223
+
161
224
  A `403` reply with `blockedByGrant: true` means the call is outside what
162
225
  the user granted. Do not retry it.
163
226
 
@@ -166,18 +229,19 @@ Errors are `OneConnectError` with a `code`. Handle them where the app calls One:
166
229
  | `code` | Meaning | Do |
167
230
  |---|---|---|
168
231
  | `not_connected` | Nothing is stored for this user. | Show the Connect button. |
169
- | `reconnect_required` | One will not act for this user: they revoked access, or the app is deactivated. The stored value is kept. | Ask the user to connect again. |
232
+ | `reconnect_required` | Key mode: One will not act for this user; they revoked access, or the app is deactivated. The stored value is kept. | Ask the user to connect again. |
233
+ | `refresh_failed` | Token mode: the grant is gone. The tokens are cleared. | Ask the user to connect again. |
170
234
  | `request_failed` | One answered with an error or could not be reached. Nothing stored changed. | Retry later. |
171
235
 
172
236
  ```ts
173
237
  import { OneConnectError } from "@withone/connect/server";
174
238
 
175
239
  try {
176
- await oneConnect.runAction(userId, action);
240
+ await oneConnect.runAction(userId, input);
177
241
  } catch (error) {
178
242
  if (
179
243
  error instanceof OneConnectError &&
180
- ["not_connected", "reconnect_required"].includes(error.code)
244
+ ["not_connected", "reconnect_required", "refresh_failed"].includes(error.code)
181
245
  ) {
182
246
  // show the Connect button again
183
247
  } else {
@@ -187,25 +251,29 @@ try {
187
251
  ```
188
252
 
189
253
  `isConnected(userId)` says the user has connected before. One confirms the
190
- consent on each call, so a user who revoked is found by the next call
191
- throwing `reconnect_required`.
254
+ consent on each call, so a user who revoked is found by the next call.
255
+
256
+ ## 9 - Rules and done
192
257
 
193
- ## 7 - Rules
258
+ Rules:
194
259
 
195
260
  - Never put the client secret or the connect key in browser code, logs,
196
261
  error reports, source files or prompts. Environment variables only.
197
262
  - Connect keys work in Production only; the human creates the key with
198
263
  the dashboard on Production.
199
264
  - Store the per-user string as given.
200
- - The registered redirect URI and `ONE_REDIRECT_URI` must be identical.
201
- - Do not build a completion page; the callback redirect is the completion.
265
+ - The registered redirect URI and `ONE_REDIRECT_URI` must be identical;
266
+ otherwise One answers `invalid redirect_uri` and never shows its page.
267
+ - A flow that ends with `failed` is usually a wrong or rotated client
268
+ secret; log `result.message` from `onComplete` to see why.
202
269
  - Do not write OAuth steps or One request headers by hand; use the package.
203
- - Do not switch an existing app from one mode to the other unless asked.
270
+ - Never show `result.message` or any URL text to users; log it.
204
271
 
205
- ## 8 - Done when
272
+ Done when:
206
273
 
207
274
  1. The button leads to One's page; after signing in and authorizing, the user
208
- lands back in the app and `onSuccess` fires.
275
+ lands back in the app and `onSuccess` fires (or the app reads
276
+ `?one_connect=success`).
209
277
  2. One string is saved for the user.
210
278
  3. `listConnections` returns only the granted connections.
211
279
  4. `runAction` works for an action inside the grant and returns `403` with
@@ -213,11 +281,10 @@ throwing `reconnect_required`.
213
281
  5. After the user revokes the app in their One dashboard, the next call
214
282
  fails with `reconnect_required` and the app asks them to connect again.
215
283
 
216
- ## 9 - Token mode (only when asked)
284
+ ## 10 - Token mode (only when chosen in section 1)
217
285
 
218
- The other way to hold the grant: the app stores an access token and a
219
- refresh token per user, as a standard OAuth client. The routes, the button
220
- and the calls are the same. No `ONE_CONNECT_KEY` is needed.
286
+ The app stores an access token and a refresh token per user, as a standard
287
+ OAuth client. No `ONE_CONNECT_KEY` is needed; everything else above holds.
221
288
 
222
289
  ```ts
223
290
  export const oneConnect = createOneConnect({
@@ -226,11 +293,13 @@ export const oneConnect = createOneConnect({
226
293
  redirectUri: process.env.ONE_REDIRECT_URI!,
227
294
  permissionSet: process.env.ONE_PERMISSION_SET,
228
295
  tokenStore: {
229
- saveTokens: (userId, tokens) => /* save in the app's database, encrypted */,
230
- loadTokens: (userId) => /* read; null when never connected */,
231
- clearTokens: (userId) => /* delete */,
296
+ saveTokens: async (userId, tokens) => { /* save in the app's database, encrypted */ },
297
+ loadTokens: async (userId) => /* read; null when never connected */,
298
+ clearTokens: async (userId) => { /* delete */ },
232
299
  },
233
300
  });
301
+
302
+ const accessToken = await oneConnect.getAccessToken(userId); // when the app needs the bearer token itself
234
303
  ```
235
304
 
236
305
  - Store tokens encrypted, keyed by the app's user.
@@ -263,6 +332,3 @@ the pair. The access token lives as long as the app's Token lifetime says
263
332
  (30 days unless changed under Advanced when creating the app); keep the
264
333
  window shorter than that, or every run refreshes. In token mode, "done"
265
334
  also means the daily job exists and runs for every connected user.
266
-
267
- The mode is whichever credential `createOneConnect` is given: `connectKey`
268
- and `userStore` for key mode, `tokenStore` for token mode.
package/src/flow.ts CHANGED
@@ -156,12 +156,6 @@ export function createConnectFlow(
156
156
  };
157
157
  }
158
158
 
159
- /** @deprecated Renamed to `createConnectFlow` (it is not a React hook;
160
- * React apps can use `useOneConnect` from `@withone/connect/react`).
161
- * Removed in the next minor. */
162
- export const useOneConnect = (options: OneConnectFlowOptions): OneConnectFlow =>
163
- createConnectFlow(options);
164
-
165
159
  /** Test seam: forget this page load's outcome. */
166
160
  export function resetPageReturnForTests(): void {
167
161
  pageReturn = undefined;
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { createConnectFlow, readConnectReturn, useOneConnect } from "./flow";
1
+ export { createConnectFlow, readConnectReturn } from "./flow";
2
2
  export {
3
3
  mountConnectButton,
4
4
  registerConnectButton,
@@ -22,8 +22,3 @@ export type {
22
22
  OneConnectTheme,
23
23
  } from "./types";
24
24
 
25
- import type { OneConnectFlow, OneConnectFlowOptions } from "./types";
26
- /** @deprecated Renamed to `OneConnectFlowOptions`; removed in the next minor. */
27
- export type OneConnectOptions = OneConnectFlowOptions;
28
- /** @deprecated Renamed to `OneConnectFlow`; removed in the next minor. */
29
- export type OneConnectHandle = OneConnectFlow;
package/src/next.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * That serves /api/one/authorize and /api/one/callback. Register
15
15
  * `https://yourapp.com/api/one/callback` as the app's redirect URI.
16
16
  */
17
- import type { OneConnectClient } from "./server";
17
+ import type { CompleteAuthorizationResult, OneConnectClient } from "./server";
18
18
 
19
19
  export interface OneConnectRoutesOptions {
20
20
  /** The app's own id for the signed-in user, or null when nobody is
@@ -25,6 +25,15 @@ export interface OneConnectRoutesOptions {
25
25
  /** Where to send the browser when nobody is signed in. Answers 401
26
26
  * when omitted. */
27
27
  signInUrl?: string;
28
+ /** Called after every callback, before the browser is redirected:
29
+ * `result.outcome` is "connected", "declined" or "failed", and
30
+ * `result.message` says why a flow failed. For your logs and metrics;
31
+ * never show the message to the user. An error it throws is ignored,
32
+ * so the user still lands back in the app. */
33
+ onComplete?: (event: {
34
+ userId: string;
35
+ result: CompleteAuthorizationResult;
36
+ }) => void | Promise<void>;
28
37
  }
29
38
 
30
39
  export interface OneConnectRoutes {
@@ -114,6 +123,13 @@ export function createOneConnectRoutes(
114
123
  url: request.url,
115
124
  getCookie: (name) => readCookie(request, name),
116
125
  });
126
+ if (options.onComplete) {
127
+ try {
128
+ await options.onComplete({ userId: user, result });
129
+ } catch {
130
+ /* the app's own hook threw; the redirect still happens */
131
+ }
132
+ }
117
133
  return redirectWithCookies(
118
134
  result.redirectUrl,
119
135
  result.clearCookieName
package/src/node.ts CHANGED
@@ -29,8 +29,12 @@ export interface OneConnectNodeOptions {
29
29
  loginHintFor?: (
30
30
  request: IncomingMessage,
31
31
  ) => Promise<string | null> | string | null;
32
- /** Where to send the browser when nobody is signed in. */
32
+ /** Where to send the browser when nobody is signed in. Answers 401
33
+ * when omitted. */
33
34
  signInUrl?: string;
35
+ /** Called after every callback with how it ended; see
36
+ * `OneConnectRoutesOptions.onComplete`. */
37
+ onComplete?: OneConnectRoutesOptions["onComplete"];
34
38
  }
35
39
 
36
40
  export type NodeHandler = (
@@ -80,27 +84,31 @@ export function createOneConnectHandlers(
80
84
  options: OneConnectNodeOptions,
81
85
  ): OneConnectHandlers {
82
86
  // The web adapter receives a Request; the Node callbacks want the
83
- // original IncomingMessage, so it is carried alongside by closure.
84
- let current: IncomingMessage | null = null;
85
- const routeOptions: OneConnectRoutesOptions = {
86
- identifyUser: () => options.identifyUser(current as IncomingMessage),
87
- loginHintFor: options.loginHintFor
88
- ? () => options.loginHintFor!(current as IncomingMessage)
87
+ // original IncomingMessage. Each Request maps to its own message, so
88
+ // concurrent requests never read each other's session or login hint.
89
+ const origin = new WeakMap<Request, IncomingMessage>();
90
+ const nodeRequestFor = (request: Request): IncomingMessage => {
91
+ const message = origin.get(request);
92
+ if (!message) throw new Error("@withone/connect/node: unknown request");
93
+ return message;
94
+ };
95
+ const loginHintFor = options.loginHintFor;
96
+ const routes = createOneConnectRoutes(oneConnect, {
97
+ identifyUser: (request) => options.identifyUser(nodeRequestFor(request)),
98
+ loginHintFor: loginHintFor
99
+ ? (request) => loginHintFor(nodeRequestFor(request))
89
100
  : undefined,
90
101
  signInUrl: options.signInUrl,
91
- };
92
- const routes = createOneConnectRoutes(oneConnect, routeOptions);
102
+ onComplete: options.onComplete,
103
+ } satisfies OneConnectRoutesOptions);
93
104
 
94
105
  const handle = async (
95
106
  request: IncomingMessage,
96
107
  response: ServerResponse,
97
108
  ): Promise<void> => {
98
- current = request;
99
- try {
100
- await send(response, await routes.GET(toWebRequest(request)));
101
- } finally {
102
- current = null;
103
- }
109
+ const web = toWebRequest(request);
110
+ origin.set(web, request);
111
+ await send(response, await routes.GET(web));
104
112
  };
105
113
 
106
114
  return { authorize: handle, callback: handle };
@@ -121,7 +121,7 @@ export interface OneConnectBaseConfig {
121
121
  export interface OneConnectKeyConfig extends OneConnectBaseConfig {
122
122
  mode?: "key";
123
123
  /** The app's connect key, minted on the app's page in the dashboard.
124
- * Server only. One key per environment. */
124
+ * Server only. Connect keys work in Production. */
125
125
  connectKey: string;
126
126
  userStore: OneConnectUserStore;
127
127
  tokenStore?: never;
package/src/types.ts CHANGED
@@ -75,9 +75,9 @@ export interface ConnectButtonLogo {
75
75
  imageUrl: string;
76
76
  }
77
77
 
78
- /** @deprecated Renamed to `ConnectButtonLogoInput`; removed in the next minor. */
78
+ /** @deprecated Renamed to `ConnectButtonLogoInput`; removed in 0.17.0. */
79
79
  export type ConnectButtonPlatformInput = ConnectButtonLogoInput;
80
- /** @deprecated Renamed to `ConnectButtonLogo`; removed in the next minor. */
80
+ /** @deprecated Renamed to `ConnectButtonLogo`; removed in 0.17.0. */
81
81
  export type ConnectButtonPlatform = ConnectButtonLogo;
82
82
 
83
83
  export type ConnectButtonVariant = "default" | "accent" | "block";
@@ -94,7 +94,7 @@ export interface ConnectButtonProps {
94
94
  * into a "+N" chip. Decoration only; the permission set decides what
95
95
  * One asks for. */
96
96
  logos?: ConnectButtonLogoInput[];
97
- /** @deprecated Renamed to `logos`; removed in the next minor. */
97
+ /** @deprecated Renamed to `logos`; removed in 0.17.0. */
98
98
  platforms?: ConnectButtonLogoInput[];
99
99
  /** Whether this user has a live grant, from your server. When set, it
100
100
  * decides the Connected state. When omitted, the button shows