@withone/connect 0.13.0 → 0.13.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 +63 -64
- package/package.json +1 -1
- package/skills/one-connect/SKILL.md +104 -107
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ You add three things: a button, two backend routes, and the calls you make with
|
|
|
28
28
|
Browser Your server One
|
|
29
29
|
<ConnectButton> ──────► GET /api/one/authorize ──302──► hosted page: sign in, pick tools, set access
|
|
30
30
|
GET /api/one/callback ◄──302── ?code&state
|
|
31
|
-
saves
|
|
31
|
+
saves the grant for the user, redirects home
|
|
32
32
|
onSuccess() ◄──────
|
|
33
33
|
later: oneConnect.runAction(userId, …) ──► One, grant enforced
|
|
34
34
|
```
|
|
@@ -45,15 +45,11 @@ Using a coding agent? `npx skills add withoneai/connect` teaches it the whole se
|
|
|
45
45
|
|
|
46
46
|
Dashboard → **Developers → Connect → New app**. Register your callback URL exactly, for example `https://yourapp.com/api/one/callback`. Optionally choose the tools and access levels to ask for; the consent page lists them in the order you add them.
|
|
47
47
|
|
|
48
|
-
Then, on the app's page, under **Credentials → Connect key**, create a key. It is shown once, and it is made for the environment your dashboard is on: switch the dashboard to Sandbox to create a Sandbox key.
|
|
49
|
-
|
|
50
48
|
```env
|
|
51
49
|
ONE_CLIENT_ID=…
|
|
52
50
|
ONE_CLIENT_SECRET=one_secret_… # server only
|
|
53
|
-
ONE_CONNECT_KEY=sk_live_… # server only: the app's connect key
|
|
54
51
|
ONE_REDIRECT_URI=https://yourapp.com/api/one/callback # exactly the registered URL
|
|
55
52
|
ONE_PERMISSION_SET=… # optional: the tools you ask for
|
|
56
|
-
ONE_API_URL=https://api.withone.ai # optional: production when unset
|
|
57
53
|
```
|
|
58
54
|
|
|
59
55
|
## 2 · The button
|
|
@@ -100,19 +96,9 @@ Your own button in React: `const { open, status } = useOneConnect({ authorizeUrl
|
|
|
100
96
|
|
|
101
97
|
To match your design, set `--one-connect-font` and `--one-connect-radius`, or style `::part(button)`.
|
|
102
98
|
|
|
103
|
-
## 3 ·
|
|
104
|
-
|
|
105
|
-
Your server holds a user's grant in one of two ways. The button, the two routes and every call are the same in both. **The mode is whichever credential you configure.**
|
|
106
|
-
|
|
107
|
-
| | Key mode (default) | Token mode |
|
|
108
|
-
|---|---|---|
|
|
109
|
-
| Your server holds | one connect key for the app | nothing app-wide |
|
|
110
|
-
| You store per user | one id, saved once | an access token and a refresh token |
|
|
111
|
-
| Expires | nothing expires; One checks the consent on every call | the tokens expire, on the lifetime set on your app |
|
|
112
|
-
| Refreshing | none | the SDK does it; several servers need a shared lock |
|
|
113
|
-
| Pick it when | you call One from your own server. Start here. | you need a standard OAuth bearer token |
|
|
99
|
+
## 3 · The server client
|
|
114
100
|
|
|
115
|
-
|
|
101
|
+
Your server keeps an access token and a refresh token for each user who connects. This is **token mode**, and it is the one to use.
|
|
116
102
|
|
|
117
103
|
```ts
|
|
118
104
|
// lib/one.ts
|
|
@@ -123,28 +109,7 @@ export const oneConnect = createOneConnect({
|
|
|
123
109
|
clientSecret: process.env.ONE_CLIENT_SECRET!,
|
|
124
110
|
redirectUri: process.env.ONE_REDIRECT_URI!,
|
|
125
111
|
permissionSet: process.env.ONE_PERMISSION_SET,
|
|
126
|
-
|
|
127
|
-
connectKey: process.env.ONE_CONNECT_KEY!, // ← this makes it key mode
|
|
128
|
-
userStore: {
|
|
129
|
-
saveUser: (userId, reference) => db.users.update(userId, { oneConnect: reference }),
|
|
130
|
-
loadUser: async (userId) => (await db.users.find(userId))?.oneConnect ?? null,
|
|
131
|
-
clearUser: (userId) => db.users.update(userId, { oneConnect: null }),
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
`reference` is one short string: the user's permanent One id, plus the space they granted from. Save it in one column and hand it back. It is an identifier, not a secret, and it stays the same if the user disconnects and connects again.
|
|
137
|
-
|
|
138
|
-
**Token mode**: pass a token store instead.
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
export const oneConnect = createOneConnect({
|
|
142
|
-
clientId: process.env.ONE_CLIENT_ID!,
|
|
143
|
-
clientSecret: process.env.ONE_CLIENT_SECRET!,
|
|
144
|
-
redirectUri: process.env.ONE_REDIRECT_URI!,
|
|
145
|
-
permissionSet: process.env.ONE_PERMISSION_SET,
|
|
146
|
-
oneApiUrl: process.env.ONE_API_URL,
|
|
147
|
-
tokenStore: { // ← this makes it token mode
|
|
112
|
+
tokenStore: {
|
|
148
113
|
saveTokens: (userId, tokens) => db.oneTokens.upsert(userId, tokens),
|
|
149
114
|
loadTokens: (userId) => db.oneTokens.find(userId),
|
|
150
115
|
clearTokens: (userId) => db.oneTokens.delete(userId),
|
|
@@ -152,11 +117,11 @@ export const oneConnect = createOneConnect({
|
|
|
152
117
|
});
|
|
153
118
|
```
|
|
154
119
|
|
|
155
|
-
|
|
120
|
+
Store the tokens in your database, encrypted, keyed by your user id.
|
|
156
121
|
|
|
157
|
-
|
|
122
|
+
Running more than one server or a background worker? Add `withLock(userId, run)` to your token store, for example a Postgres advisory lock. Two servers refreshing at once would otherwise disconnect the user.
|
|
158
123
|
|
|
159
|
-
|
|
124
|
+
## 4 · The two routes
|
|
160
125
|
|
|
161
126
|
```ts
|
|
162
127
|
// app/api/one/[action]/route.ts (Next.js; also Remix, SvelteKit, Hono, Bun)
|
|
@@ -172,8 +137,6 @@ That file serves `/api/one/authorize` and `/api/one/callback`. For Express or pl
|
|
|
172
137
|
|
|
173
138
|
## 5 · Using the grant
|
|
174
139
|
|
|
175
|
-
The same calls in both modes:
|
|
176
|
-
|
|
177
140
|
```ts
|
|
178
141
|
const connections = await oneConnect.listConnections(userId); // what the user granted
|
|
179
142
|
const actions = await oneConnect.listActions(userId, "gmail"); // what a platform can do
|
|
@@ -184,38 +147,74 @@ const reply = await oneConnect.runAction(userId, {
|
|
|
184
147
|
method: actions[0].method,
|
|
185
148
|
path: actions[0].path,
|
|
186
149
|
});
|
|
187
|
-
// { status, ok,
|
|
150
|
+
// { status, ok, data }
|
|
188
151
|
```
|
|
189
152
|
|
|
190
|
-
|
|
153
|
+
You never set a header: the client adds the user's credential to every call it makes.
|
|
154
|
+
|
|
155
|
+
A `403` means the call is outside what the user granted. Don't retry it.
|
|
191
156
|
|
|
192
157
|
Errors are `OneConnectError` with a `code`:
|
|
193
158
|
|
|
194
|
-
| `code` |
|
|
195
|
-
|
|
196
|
-
| `not_connected` |
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
|
|
159
|
+
| `code` | What it means | What to do |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `not_connected` | Nothing is stored for this user. | Show the Connect button. |
|
|
162
|
+
| `refresh_failed` | The grant ended (revoked or expired). The tokens were cleared. | Ask the user to connect again. |
|
|
163
|
+
| `request_failed` | One answered with an error or could not be reached. Nothing stored changed. | Retry later. |
|
|
164
|
+
|
|
165
|
+
## 6 · Keeping users connected
|
|
200
166
|
|
|
201
|
-
|
|
167
|
+
Tokens expire, and two things keep them fresh. One is automatic. The other is yours to run.
|
|
202
168
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
- Lost or leaked a key? Create another on the app's page and delete the old one under **Developers → API keys**.
|
|
169
|
+
| | Who does it | When |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| Refresh before a call | the client, on its own | whenever you call One and the token is about to expire |
|
|
172
|
+
| Refresh for users who have not called in a while | **you**, with a daily job | once a day, for every connected user |
|
|
208
173
|
|
|
209
|
-
|
|
174
|
+
```ts
|
|
175
|
+
// run once a day
|
|
176
|
+
for (const userId of await db.oneTokens.allUserIds()) {
|
|
177
|
+
await oneConnect.refreshIfExpiring(userId, { withinMs: 3 * 24 * 3_600_000 });
|
|
178
|
+
}
|
|
179
|
+
```
|
|
210
180
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
- Required: once a day, for each connected user, call `oneConnect.refreshIfExpiring(userId, { withinMs: 3 * 24 * 3_600_000 })`. It renews both tokens when either is within 3 days of expiring. Without it, users have to connect again when the tokens run out. Keep the window shorter than your Token lifetime, or every run refreshes.
|
|
215
|
-
- `getAccessToken`, `getTokens`, `refreshTokens` and `refreshIfExpiring` exist in token mode only.
|
|
181
|
+
The job renews both tokens when either is within 3 days of expiring. Without it, a user who stays away for 30 days has to connect again: the refresh token lives 30 days, and only a live refresh token can renew the pair.
|
|
182
|
+
|
|
183
|
+
The access token lives as long as your app's **Token lifetime** says: 30 days unless you changed it under **Advanced** when creating or editing the app. Keep the job's window shorter than that, or every run refreshes.
|
|
216
184
|
|
|
217
185
|
To ask for more tools later, edit your app's tools in the dashboard. Users see only the new ones the next time they connect.
|
|
218
186
|
|
|
187
|
+
## 7 · Key mode
|
|
188
|
+
|
|
189
|
+
A second way to hold a grant: one **connect key** for your app and one permanent id per user, with nothing to refresh. The button, the routes and the calls stay the same.
|
|
190
|
+
|
|
191
|
+
Two limits today, so use token mode unless you have a reason not to:
|
|
192
|
+
|
|
193
|
+
- `listActions` does not work in key mode yet. Your app has to know the actions it runs.
|
|
194
|
+
- It works in Production only. Create the key with your dashboard on Production.
|
|
195
|
+
|
|
196
|
+
Create the key on your app's page, under **Credentials → Connect key**. It is shown once. Keep it on the server as `ONE_CONNECT_KEY`.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
export const oneConnect = createOneConnect({
|
|
200
|
+
clientId: process.env.ONE_CLIENT_ID!,
|
|
201
|
+
clientSecret: process.env.ONE_CLIENT_SECRET!,
|
|
202
|
+
redirectUri: process.env.ONE_REDIRECT_URI!,
|
|
203
|
+
permissionSet: process.env.ONE_PERMISSION_SET,
|
|
204
|
+
connectKey: process.env.ONE_CONNECT_KEY!,
|
|
205
|
+
userStore: {
|
|
206
|
+
saveUser: (userId, reference) => db.users.update(userId, { oneConnect: reference }),
|
|
207
|
+
loadUser: async (userId) => (await db.users.find(userId))?.oneConnect ?? null,
|
|
208
|
+
clearUser: (userId) => db.users.update(userId, { oneConnect: null }),
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
- `reference` is one short string. Save it in one column and hand it back unchanged. It is an identifier, not a secret.
|
|
214
|
+
- When a user revokes access, the next call throws `reconnect_required` instead of `refresh_failed`. Ask them to connect again; your stored value is kept.
|
|
215
|
+
- A `403` reply carries `blockedByGrant: true` when the call is outside what the user granted.
|
|
216
|
+
- The mode is whichever credential you pass: `tokenStore` for token mode, `connectKey` and `userStore` for key mode. `oneConnect.mode` tells you which one is running.
|
|
217
|
+
|
|
219
218
|
## License
|
|
220
219
|
|
|
221
220
|
GPL-3.0. See [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withone/connect",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "One Connect for your app: the button your users press, the two backend routes as one import, and a server client that calls One with the grant. Users keep their connections in One; your app holds only what they granted.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -1,24 +1,29 @@
|
|
|
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,
|
|
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, storing and refreshing the grant, and calling One with it.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# One Connect
|
|
7
7
|
|
|
8
8
|
You are adding One Connect to this application. Its users will grant the app
|
|
9
9
|
scoped, revocable access to their own tools. The package does the OAuth work;
|
|
10
|
-
you wire
|
|
10
|
+
you wire four things: a button, two routes, a daily refresh job, and the calls
|
|
11
|
+
made with the grant.
|
|
11
12
|
|
|
12
13
|
```
|
|
13
14
|
Browser Your backend One
|
|
14
15
|
<ConnectButton> ------> GET /api/one/authorize ---302---> hosted page: sign in, pick tools, set access
|
|
15
16
|
GET /api/one/callback <--302---- ?code&state
|
|
16
|
-
|
|
17
|
+
stores the tokens, redirects home
|
|
17
18
|
<------ onSuccess fires
|
|
18
19
|
Later: oneConnect.runAction(userId, ...) -> One, grant enforced
|
|
20
|
+
Daily: oneConnect.refreshIfExpiring(userId, ...) for every connected user
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
Use **token mode**, described in sections 1 to 8. Key mode (section 9) is a
|
|
24
|
+
second way to hold the grant; use it only when the human asks for it.
|
|
25
|
+
|
|
26
|
+
Secrets and tokens stay on the server.
|
|
22
27
|
|
|
23
28
|
## 1 - Ask the human for these
|
|
24
29
|
|
|
@@ -28,64 +33,27 @@ They create the app in the One dashboard: Developers -> Connect -> New app.
|
|
|
28
33
|
|---|---|
|
|
29
34
|
| `ONE_CLIENT_ID` | From the app. |
|
|
30
35
|
| `ONE_CLIENT_SECRET` | Starts with `one_secret_`. Shown once. |
|
|
31
|
-
| `ONE_CONNECT_KEY` | Key mode only. On the app's page: Credentials -> Connect key -> Create key. Shown once. It is made for the environment the dashboard is on (Production or Sandbox). |
|
|
32
36
|
| Redirect URI | Registered on the app. Must match the callback route exactly, e.g. `http://localhost:3000/api/one/callback`. |
|
|
33
37
|
| `ONE_PERMISSION_SET` | Optional. The tools and access levels to ask for. |
|
|
34
|
-
| `ONE_API_URL` | Optional. Production when unset; `https://development-api.withone.ai` for the development dashboard. |
|
|
35
38
|
|
|
36
|
-
Never ask the human to paste the secret
|
|
37
|
-
|
|
39
|
+
Never ask the human to paste the secret into the chat. Tell them which
|
|
40
|
+
environment variable to set and read it from there.
|
|
38
41
|
|
|
39
42
|
## 2 - Environment (server only)
|
|
40
43
|
|
|
41
44
|
```bash
|
|
42
45
|
ONE_CLIENT_ID=...
|
|
43
46
|
ONE_CLIENT_SECRET=one_secret_...
|
|
44
|
-
ONE_CONNECT_KEY=sk_live_... # key mode
|
|
45
47
|
ONE_REDIRECT_URI=https://yourapp.com/api/one/callback
|
|
46
48
|
ONE_PERMISSION_SET=... # optional
|
|
47
|
-
ONE_API_URL=... # optional
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## 3 - Choose a mode
|
|
51
|
-
|
|
52
|
-
The server holds a user's grant in one of two ways. The button, the two
|
|
53
|
-
routes and every call are identical in both. The mode is whichever
|
|
54
|
-
credential you pass to `createOneConnect`.
|
|
55
|
-
|
|
56
|
-
| | Key mode (default) | Token mode |
|
|
57
|
-
|---|---|---|
|
|
58
|
-
| The server holds | one connect key for the app | nothing app-wide |
|
|
59
|
-
| Stored per user | one string, saved once | an access token and a refresh token |
|
|
60
|
-
| Expires | nothing; One checks the consent on every call | the tokens, on the lifetime set on the app |
|
|
61
|
-
| Refreshing | none | the SDK does it; several servers need a shared lock |
|
|
62
|
-
|
|
63
|
-
How to decide:
|
|
64
|
-
|
|
65
|
-
- New integration: **key mode**.
|
|
66
|
-
- The app already passes a `tokenStore`: it is in token mode. Leave it as it
|
|
67
|
-
is unless the human asks to move.
|
|
68
|
-
- The human asks for OAuth bearer tokens: token mode.
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
// key mode
|
|
72
|
-
createOneConnect({ ...app, connectKey: process.env.ONE_CONNECT_KEY!, userStore });
|
|
73
|
-
|
|
74
|
-
// token mode
|
|
75
|
-
createOneConnect({ ...app, tokenStore });
|
|
76
49
|
```
|
|
77
50
|
|
|
78
|
-
|
|
79
|
-
`oneConnect.mode` reports which one is running.
|
|
80
|
-
|
|
81
|
-
## 4 - Install and create the client
|
|
51
|
+
## 3 - Install and create the client
|
|
82
52
|
|
|
83
53
|
```bash
|
|
84
54
|
npm install @withone/connect
|
|
85
55
|
```
|
|
86
56
|
|
|
87
|
-
Key mode:
|
|
88
|
-
|
|
89
57
|
```ts
|
|
90
58
|
// lib/one.ts (server only)
|
|
91
59
|
import { createOneConnect } from "@withone/connect/server";
|
|
@@ -95,30 +63,6 @@ export const oneConnect = createOneConnect({
|
|
|
95
63
|
clientSecret: process.env.ONE_CLIENT_SECRET!,
|
|
96
64
|
redirectUri: process.env.ONE_REDIRECT_URI!,
|
|
97
65
|
permissionSet: process.env.ONE_PERMISSION_SET,
|
|
98
|
-
oneApiUrl: process.env.ONE_API_URL,
|
|
99
|
-
connectKey: process.env.ONE_CONNECT_KEY!,
|
|
100
|
-
userStore: {
|
|
101
|
-
saveUser: (userId, reference) => /* save the string on the app's user row */,
|
|
102
|
-
loadUser: (userId) => /* read it; null when never connected */,
|
|
103
|
-
clearUser: (userId) => /* set it to null */,
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
`reference` is one short string: the user's permanent One id and the space
|
|
109
|
-
they granted from. One text column on the user row is enough. Store it as
|
|
110
|
-
given and hand it back unchanged; do not parse or rebuild it. It is an
|
|
111
|
-
identifier, not a secret, so it needs no encryption and no lock.
|
|
112
|
-
|
|
113
|
-
Token mode:
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
export const oneConnect = createOneConnect({
|
|
117
|
-
clientId: process.env.ONE_CLIENT_ID!,
|
|
118
|
-
clientSecret: process.env.ONE_CLIENT_SECRET!,
|
|
119
|
-
redirectUri: process.env.ONE_REDIRECT_URI!,
|
|
120
|
-
permissionSet: process.env.ONE_PERMISSION_SET,
|
|
121
|
-
oneApiUrl: process.env.ONE_API_URL,
|
|
122
66
|
tokenStore: {
|
|
123
67
|
saveTokens: (userId, tokens) => /* save in the app's database, encrypted */,
|
|
124
68
|
loadTokens: (userId) => /* read; null when never connected */,
|
|
@@ -127,21 +71,13 @@ export const oneConnect = createOneConnect({
|
|
|
127
71
|
});
|
|
128
72
|
```
|
|
129
73
|
|
|
130
|
-
|
|
131
|
-
worker, also add `withLock: (userId, run) => ...`, which runs `run()` while
|
|
132
|
-
holding a per-user lock all processes share (for example a Postgres advisory
|
|
133
|
-
lock). The access token lives as long as the app's Token lifetime says (30
|
|
134
|
-
days unless changed under Advanced when creating the app); the refresh token
|
|
135
|
-
lives 30 days. Required: once a day, for each connected user, call
|
|
136
|
-
`oneConnect.refreshIfExpiring(userId, { withinMs: 3 * 24 * 3_600_000 })`. It
|
|
137
|
-
renews both tokens when either is within 3 days of expiring; without it,
|
|
138
|
-
users have to connect again when the tokens run out. Keep the window shorter
|
|
139
|
-
than the Token lifetime, or every run refreshes.
|
|
74
|
+
Use the app's own user id as the key and the database it already has.
|
|
140
75
|
|
|
141
|
-
|
|
142
|
-
|
|
76
|
+
If the app runs more than one server process or a background worker, also
|
|
77
|
+
add `withLock: (userId, run) => ...`, which runs `run()` while holding a
|
|
78
|
+
per-user lock all processes share (for example a Postgres advisory lock).
|
|
143
79
|
|
|
144
|
-
##
|
|
80
|
+
## 4 - The two routes
|
|
145
81
|
|
|
146
82
|
Next.js App Router (also Remix, SvelteKit, Hono, Bun):
|
|
147
83
|
|
|
@@ -161,7 +97,7 @@ Express or plain Node: `createOneConnectHandlers(oneConnect, { identifyUser })`
|
|
|
161
97
|
from `@withone/connect/node`, mounted at `/api/one/authorize` and
|
|
162
98
|
`/api/one/callback`.
|
|
163
99
|
|
|
164
|
-
##
|
|
100
|
+
## 5 - The button
|
|
165
101
|
|
|
166
102
|
```tsx
|
|
167
103
|
import { ConnectButton } from "@withone/connect/react";
|
|
@@ -184,6 +120,32 @@ Vue: `@withone/connect/vue`, same props. Svelte: `use:connectButton` from
|
|
|
184
120
|
`<one-connect-button authorize-url="/api/one/authorize" platforms="gmail, stripe">`.
|
|
185
121
|
A custom button in React: `useOneConnect({ authorizeUrl })` returns `{ open, status }`.
|
|
186
122
|
|
|
123
|
+
## 6 - Keeping users connected
|
|
124
|
+
|
|
125
|
+
Tokens expire. Two things keep them fresh, and only the first is automatic.
|
|
126
|
+
|
|
127
|
+
| | Who does it | When |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| Refresh before a call | the package, on its own | whenever the app calls One and the token is about to expire |
|
|
130
|
+
| Refresh for users who have not called in a while | **the app**, with a daily job | once a day, for every connected user |
|
|
131
|
+
|
|
132
|
+
You must add the daily job. Without it, a user who stays away for 30 days
|
|
133
|
+
has to connect again: the refresh token lives 30 days, and only a live
|
|
134
|
+
refresh token can renew the pair.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// a scheduled job, once a day
|
|
138
|
+
for (const userId of /* every user with stored tokens */) {
|
|
139
|
+
await oneConnect.refreshIfExpiring(userId, { withinMs: 3 * 24 * 3_600_000 });
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
It renews both tokens when either is within 3 days of expiring, and does
|
|
144
|
+
nothing otherwise. The access token lives as long as the app's Token lifetime
|
|
145
|
+
says (30 days unless changed under Advanced when creating the app); keep the
|
|
146
|
+
window shorter than that, or every run refreshes. Use the scheduler the app
|
|
147
|
+
already has (a cron route, a queue, a worker).
|
|
148
|
+
|
|
187
149
|
## 7 - Calling One with the grant
|
|
188
150
|
|
|
189
151
|
```ts
|
|
@@ -197,20 +159,21 @@ const reply = await oneConnect.runAction(userId, {
|
|
|
197
159
|
path: action.path,
|
|
198
160
|
body: payload,
|
|
199
161
|
});
|
|
200
|
-
// { status, ok,
|
|
162
|
+
// { status, ok, data }
|
|
201
163
|
```
|
|
202
164
|
|
|
203
|
-
|
|
204
|
-
it
|
|
165
|
+
Do not set any header. The package adds the user's credential to every call
|
|
166
|
+
it makes, and refreshes it first when it is about to expire.
|
|
167
|
+
|
|
168
|
+
A `403` reply means the call is outside what the user granted. Do not retry it.
|
|
205
169
|
|
|
206
170
|
Errors are `OneConnectError` with a `code`. Handle them where the app calls One:
|
|
207
171
|
|
|
208
|
-
| `code` |
|
|
209
|
-
|
|
210
|
-
| `not_connected` |
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `request_failed` | both | One answered with an error or could not be reached. Nothing stored changed. | Retry later. |
|
|
172
|
+
| `code` | Meaning | Do |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| `not_connected` | Nothing is stored for this user. | Show the Connect button. |
|
|
175
|
+
| `refresh_failed` | The grant ended. The tokens were cleared. | Ask the user to connect again. |
|
|
176
|
+
| `request_failed` | One answered with an error or could not be reached. Nothing stored changed. | Retry later. |
|
|
214
177
|
|
|
215
178
|
```ts
|
|
216
179
|
import { OneConnectError } from "@withone/connect/server";
|
|
@@ -220,7 +183,7 @@ try {
|
|
|
220
183
|
} catch (error) {
|
|
221
184
|
if (
|
|
222
185
|
error instanceof OneConnectError &&
|
|
223
|
-
["not_connected", "
|
|
186
|
+
["not_connected", "refresh_failed"].includes(error.code)
|
|
224
187
|
) {
|
|
225
188
|
// show the Connect button again
|
|
226
189
|
} else {
|
|
@@ -229,31 +192,65 @@ try {
|
|
|
229
192
|
}
|
|
230
193
|
```
|
|
231
194
|
|
|
232
|
-
In key mode `isConnected(userId)` says the user has connected before. One
|
|
233
|
-
confirms the consent on each call, so a user who revoked is found by the
|
|
234
|
-
next call throwing `reconnect_required`.
|
|
235
|
-
|
|
236
195
|
## 8 - Rules
|
|
237
196
|
|
|
238
|
-
- Never put the client secret
|
|
239
|
-
|
|
240
|
-
-
|
|
241
|
-
|
|
242
|
-
- Key mode: store the per-user string as given. Token mode: store tokens
|
|
243
|
-
encrypted, keyed by the app's user.
|
|
197
|
+
- Never put the client secret in browser code, logs, error reports, source
|
|
198
|
+
files or prompts. Environment variables only.
|
|
199
|
+
- Store tokens encrypted, keyed by the app's user.
|
|
200
|
+
- Add the daily refresh job. It is part of the integration, not an extra.
|
|
244
201
|
- The registered redirect URI and `ONE_REDIRECT_URI` must be identical.
|
|
245
202
|
- Do not build a completion page; the callback redirect is the completion.
|
|
246
203
|
- Do not write OAuth steps or One request headers by hand; use the package.
|
|
247
204
|
- Do not switch an existing app from one mode to the other unless asked.
|
|
248
205
|
|
|
249
|
-
## 9 -
|
|
206
|
+
## 9 - Key mode (only when asked)
|
|
207
|
+
|
|
208
|
+
A second way to hold the grant: one connect key for the app and one permanent
|
|
209
|
+
id per user, with nothing to refresh. The routes, the button and the calls are
|
|
210
|
+
the same. Two limits today:
|
|
211
|
+
|
|
212
|
+
- `listActions` does not work in key mode yet. The app has to know the
|
|
213
|
+
actions it runs.
|
|
214
|
+
- It works in Production only. The human creates the key with the dashboard
|
|
215
|
+
on Production.
|
|
216
|
+
|
|
217
|
+
The human creates the key on the app's page: Credentials -> Connect key ->
|
|
218
|
+
Create key. It is shown once. They set it as `ONE_CONNECT_KEY`; never ask
|
|
219
|
+
them to paste it into the chat.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
export const oneConnect = createOneConnect({
|
|
223
|
+
clientId: process.env.ONE_CLIENT_ID!,
|
|
224
|
+
clientSecret: process.env.ONE_CLIENT_SECRET!,
|
|
225
|
+
redirectUri: process.env.ONE_REDIRECT_URI!,
|
|
226
|
+
permissionSet: process.env.ONE_PERMISSION_SET,
|
|
227
|
+
connectKey: process.env.ONE_CONNECT_KEY!,
|
|
228
|
+
userStore: {
|
|
229
|
+
saveUser: (userId, reference) => /* save the string on the app's user row */,
|
|
230
|
+
loadUser: (userId) => /* read it; null when never connected */,
|
|
231
|
+
clearUser: (userId) => /* set it to null */,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- `reference` is one short string. Store it as given and hand it back
|
|
237
|
+
unchanged; do not parse or rebuild it. It is an identifier, not a secret.
|
|
238
|
+
- There is no daily refresh job in key mode.
|
|
239
|
+
- When the user revokes access, the next call throws `reconnect_required`
|
|
240
|
+
(not `refresh_failed`). Ask them to connect again. The stored value is kept.
|
|
241
|
+
- A `403` reply carries `blockedByGrant: true` when the call is outside what
|
|
242
|
+
the user granted.
|
|
243
|
+
- The mode is whichever credential `createOneConnect` is given: `tokenStore`
|
|
244
|
+
for token mode, `connectKey` and `userStore` for key mode.
|
|
245
|
+
|
|
246
|
+
## 10 - Done when
|
|
250
247
|
|
|
251
248
|
1. The button leads to One's page; after signing in and authorizing, the user
|
|
252
249
|
lands back in the app and `onSuccess` fires.
|
|
253
|
-
2.
|
|
250
|
+
2. The tokens are saved for the user.
|
|
254
251
|
3. `listConnections` returns only the granted connections.
|
|
255
252
|
4. `runAction` works for an action inside the grant and returns `403` for
|
|
256
253
|
one outside it.
|
|
257
|
-
5.
|
|
258
|
-
|
|
259
|
-
|
|
254
|
+
5. The daily refresh job exists and runs for every connected user.
|
|
255
|
+
6. After the user revokes the app in their One dashboard, the app asks them
|
|
256
|
+
to connect again.
|