@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.
- package/README.md +254 -53
- package/dist/flow.d.ts +0 -4
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +1 -6
- package/dist/index.esm.js +1 -1
- package/dist/next.cjs.js +10 -0
- package/dist/next.d.ts +10 -1
- package/dist/next.esm.js +10 -0
- package/dist/node.cjs.js +17 -13
- package/dist/node.d.ts +6 -1
- package/dist/node.esm.js +17 -13
- package/dist/server/types.d.ts +1 -1
- package/dist/types.d.ts +3 -3
- package/package.json +1 -1
- package/skills/one-connect/SKILL.md +117 -51
- package/src/flow.ts +0 -6
- package/src/index.ts +1 -6
- package/src/next.ts +17 -1
- package/src/node.ts +23 -15
- package/src/server/types.ts +1 -1
- package/src/types.ts +3 -3
|
@@ -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
|
|
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
|
|
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 -
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
114
|
+
onComplete: ({ userId, result }) => { /* log result.outcome and, on failure, result.message */ },
|
|
99
115
|
});
|
|
100
116
|
```
|
|
101
117
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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"),
|
|
122
|
-
`
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
270
|
+
- Never show `result.message` or any URL text to users; log it.
|
|
204
271
|
|
|
205
|
-
|
|
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
|
-
##
|
|
284
|
+
## 10 - Token mode (only when chosen in section 1)
|
|
217
285
|
|
|
218
|
-
The
|
|
219
|
-
|
|
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
|
|
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
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 };
|
package/src/server/types.ts
CHANGED
|
@@ -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.
|
|
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
|
|
78
|
+
/** @deprecated Renamed to `ConnectButtonLogoInput`; removed in 0.17.0. */
|
|
79
79
|
export type ConnectButtonPlatformInput = ConnectButtonLogoInput;
|
|
80
|
-
/** @deprecated Renamed to `ConnectButtonLogo`; removed in
|
|
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
|
|
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
|