@withone/connect 0.13.1 → 0.13.2
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 +49 -49
- package/package.json +1 -1
- package/skills/one-connect/SKILL.md +89 -90
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 one id for the user, redirects home
|
|
32
32
|
onSuccess() ◄──────
|
|
33
33
|
later: oneConnect.runAction(userId, …) ──► One, grant enforced
|
|
34
34
|
```
|
|
@@ -45,9 +45,12 @@ 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 keys**, create a key with your dashboard on Production. It is shown once.
|
|
49
|
+
|
|
48
50
|
```env
|
|
49
51
|
ONE_CLIENT_ID=…
|
|
50
52
|
ONE_CLIENT_SECRET=one_secret_… # server only
|
|
53
|
+
ONE_CONNECT_KEY=sk_live_… # server only
|
|
51
54
|
ONE_REDIRECT_URI=https://yourapp.com/api/one/callback # exactly the registered URL
|
|
52
55
|
ONE_PERMISSION_SET=… # optional: the tools you ask for
|
|
53
56
|
```
|
|
@@ -98,7 +101,7 @@ To match your design, set `--one-connect-font` and `--one-connect-radius`, or st
|
|
|
98
101
|
|
|
99
102
|
## 3 · The server client
|
|
100
103
|
|
|
101
|
-
Your server
|
|
104
|
+
Your server holds one **connect key** for the app and saves one id per user who connects. Nothing expires and nothing is refreshed: One checks the user's consent on every call. This is **key mode**, the default.
|
|
102
105
|
|
|
103
106
|
```ts
|
|
104
107
|
// lib/one.ts
|
|
@@ -109,17 +112,16 @@ export const oneConnect = createOneConnect({
|
|
|
109
112
|
clientSecret: process.env.ONE_CLIENT_SECRET!,
|
|
110
113
|
redirectUri: process.env.ONE_REDIRECT_URI!,
|
|
111
114
|
permissionSet: process.env.ONE_PERMISSION_SET,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
connectKey: process.env.ONE_CONNECT_KEY!,
|
|
116
|
+
userStore: {
|
|
117
|
+
saveUser: (userId, reference) => db.users.update(userId, { oneConnect: reference }),
|
|
118
|
+
loadUser: async (userId) => (await db.users.find(userId))?.oneConnect ?? null,
|
|
119
|
+
clearUser: (userId) => db.users.update(userId, { oneConnect: null }),
|
|
116
120
|
},
|
|
117
121
|
});
|
|
118
122
|
```
|
|
119
123
|
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
124
|
+
`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 unchanged. It is an identifier, not a secret, and it stays the same if the user disconnects and connects again.
|
|
123
125
|
|
|
124
126
|
## 4 · The two routes
|
|
125
127
|
|
|
@@ -147,53 +149,34 @@ const reply = await oneConnect.runAction(userId, {
|
|
|
147
149
|
method: actions[0].method,
|
|
148
150
|
path: actions[0].path,
|
|
149
151
|
});
|
|
150
|
-
// { status, ok, data }
|
|
152
|
+
// { status, ok, blockedByGrant, data }
|
|
151
153
|
```
|
|
152
154
|
|
|
153
|
-
You never set a header: the client adds the user's
|
|
155
|
+
You never set a header: the client adds the connect key and the user's id to every call it makes.
|
|
154
156
|
|
|
155
|
-
A `403` means the call is outside what the user granted. Don't retry it.
|
|
157
|
+
A `403` with `blockedByGrant: true` means the call is outside what the user granted. Don't retry it.
|
|
156
158
|
|
|
157
159
|
Errors are `OneConnectError` with a `code`:
|
|
158
160
|
|
|
159
161
|
| `code` | What it means | What to do |
|
|
160
162
|
|---|---|---|
|
|
161
163
|
| `not_connected` | Nothing is stored for this user. | Show the Connect button. |
|
|
162
|
-
| `
|
|
164
|
+
| `reconnect_required` | One will not act for this user: they revoked access, or the app is deactivated. Your stored value is kept. | Ask the user to connect again. |
|
|
163
165
|
| `request_failed` | One answered with an error or could not be reached. Nothing stored changed. | Retry later. |
|
|
164
166
|
|
|
165
|
-
## 6 ·
|
|
166
|
-
|
|
167
|
-
Tokens expire, and two things keep them fresh. One is automatic. The other is yours to run.
|
|
168
|
-
|
|
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 |
|
|
167
|
+
## 6 · Key mode notes
|
|
173
168
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
```
|
|
180
|
-
|
|
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.
|
|
169
|
+
- Keep the connect key on the server, in an environment variable. It is bound to your app and does nothing without a user's id.
|
|
170
|
+
- Connect keys work in Production only. Create the key with your dashboard on Production.
|
|
171
|
+
- `isConnected(userId)` says whether the user has connected before. One confirms the consent on each call, so a user who revoked is found by the next call throwing `reconnect_required`.
|
|
172
|
+
- `oneConnect.getConnectUserId(userId)` returns the user's One id (`cu_…`) if you want it for your own records.
|
|
173
|
+
- Lost or leaked a key? Create another on the app's page, move your servers to it, then revoke the old one there.
|
|
184
174
|
|
|
185
175
|
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.
|
|
186
176
|
|
|
187
|
-
## 7 ·
|
|
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.
|
|
177
|
+
## 7 · Token mode
|
|
190
178
|
|
|
191
|
-
|
|
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`.
|
|
179
|
+
The other way to hold a grant: your server stores an access token and a refresh token per user, as a standard OAuth client. The button, the routes and the calls stay the same. Use it when you need bearer tokens, or when the app already has them; an app that passes only a `tokenStore` keeps running in token mode with no change.
|
|
197
180
|
|
|
198
181
|
```ts
|
|
199
182
|
export const oneConnect = createOneConnect({
|
|
@@ -201,19 +184,36 @@ export const oneConnect = createOneConnect({
|
|
|
201
184
|
clientSecret: process.env.ONE_CLIENT_SECRET!,
|
|
202
185
|
redirectUri: process.env.ONE_REDIRECT_URI!,
|
|
203
186
|
permissionSet: process.env.ONE_PERMISSION_SET,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
clearUser: (userId) => db.users.update(userId, { oneConnect: null }),
|
|
187
|
+
tokenStore: {
|
|
188
|
+
saveTokens: (userId, tokens) => db.oneTokens.upsert(userId, tokens),
|
|
189
|
+
loadTokens: (userId) => db.oneTokens.find(userId),
|
|
190
|
+
clearTokens: (userId) => db.oneTokens.delete(userId),
|
|
209
191
|
},
|
|
210
192
|
});
|
|
211
193
|
```
|
|
212
194
|
|
|
213
|
-
-
|
|
214
|
-
-
|
|
215
|
-
-
|
|
216
|
-
-
|
|
195
|
+
- Store the tokens in your database, encrypted, keyed by your user id.
|
|
196
|
+
- 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.
|
|
197
|
+
- When a user revokes access, the next refresh throws `refresh_failed` and the tokens are cleared. Ask them to connect again.
|
|
198
|
+
- A `403` means the call is outside what the user granted; `blockedByGrant` stays `false` in token mode.
|
|
199
|
+
|
|
200
|
+
**Keeping users connected.** Tokens expire, and two things keep them fresh. One is automatic. The other is yours to run.
|
|
201
|
+
|
|
202
|
+
| | Who does it | When |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| Refresh before a call | the client, on its own | whenever you call One and the token is about to expire |
|
|
205
|
+
| Refresh for users who have not called in a while | **you**, with a daily job | once a day, for every connected user |
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
// run once a day
|
|
209
|
+
for (const userId of await db.oneTokens.allUserIds()) {
|
|
210
|
+
await oneConnect.refreshIfExpiring(userId, { withinMs: 3 * 24 * 3_600_000 });
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
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. The access token lives as long as your app's **Token lifetime** says (30 days unless you changed it under **Advanced**); keep the job's window shorter than that, or every run refreshes.
|
|
215
|
+
|
|
216
|
+
The mode is whichever credential you pass: `connectKey` and `userStore` for key mode, `tokenStore` for token mode. `oneConnect.mode` tells you which one is running.
|
|
217
217
|
|
|
218
218
|
## License
|
|
219
219
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withone/connect",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
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,29 +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, the connect key, and calling One with the grant.
|
|
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
|
|
11
|
-
made with the grant.
|
|
10
|
+
you wire three things: a button, two routes, and the calls made with the grant.
|
|
12
11
|
|
|
13
12
|
```
|
|
14
13
|
Browser Your backend One
|
|
15
14
|
<ConnectButton> ------> GET /api/one/authorize ---302---> hosted page: sign in, pick tools, set access
|
|
16
15
|
GET /api/one/callback <--302---- ?code&state
|
|
17
|
-
|
|
16
|
+
saves one id for the user, redirects home
|
|
18
17
|
<------ onSuccess fires
|
|
19
18
|
Later: oneConnect.runAction(userId, ...) -> One, grant enforced
|
|
20
|
-
Daily: oneConnect.refreshIfExpiring(userId, ...) for every connected user
|
|
21
19
|
```
|
|
22
20
|
|
|
23
|
-
Use **
|
|
24
|
-
|
|
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
25
|
|
|
26
|
-
|
|
26
|
+
The client secret and the connect key stay on the server.
|
|
27
27
|
|
|
28
28
|
## 1 - Ask the human for these
|
|
29
29
|
|
|
@@ -33,17 +33,19 @@ They create the app in the One dashboard: Developers -> Connect -> New app.
|
|
|
33
33
|
|---|---|
|
|
34
34
|
| `ONE_CLIENT_ID` | From the app. |
|
|
35
35
|
| `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. |
|
|
36
37
|
| Redirect URI | Registered on the app. Must match the callback route exactly, e.g. `http://localhost:3000/api/one/callback`. |
|
|
37
38
|
| `ONE_PERMISSION_SET` | Optional. The tools and access levels to ask for. |
|
|
38
39
|
|
|
39
|
-
Never ask the human to paste the secret into the chat.
|
|
40
|
-
environment variable to set and read it from there.
|
|
40
|
+
Never ask the human to paste the secret or the connect key into the chat.
|
|
41
|
+
Tell them which environment variable to set and read it from there.
|
|
41
42
|
|
|
42
43
|
## 2 - Environment (server only)
|
|
43
44
|
|
|
44
45
|
```bash
|
|
45
46
|
ONE_CLIENT_ID=...
|
|
46
47
|
ONE_CLIENT_SECRET=one_secret_...
|
|
48
|
+
ONE_CONNECT_KEY=sk_live_...
|
|
47
49
|
ONE_REDIRECT_URI=https://yourapp.com/api/one/callback
|
|
48
50
|
ONE_PERMISSION_SET=... # optional
|
|
49
51
|
```
|
|
@@ -63,19 +65,23 @@ export const oneConnect = createOneConnect({
|
|
|
63
65
|
clientSecret: process.env.ONE_CLIENT_SECRET!,
|
|
64
66
|
redirectUri: process.env.ONE_REDIRECT_URI!,
|
|
65
67
|
permissionSet: process.env.ONE_PERMISSION_SET,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
connectKey: process.env.ONE_CONNECT_KEY!,
|
|
69
|
+
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 */,
|
|
70
73
|
},
|
|
71
74
|
});
|
|
72
75
|
```
|
|
73
76
|
|
|
74
77
|
Use the app's own user id as the key and the database it already has.
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
`reference` is one short string: the user's permanent One id and the space
|
|
80
|
+
they granted from. One text column on the user row is enough. Store it as
|
|
81
|
+
given and hand it back unchanged; do not parse or rebuild it. It is an
|
|
82
|
+
identifier, not a secret, so it needs no encryption and no lock. The
|
|
83
|
+
package writes it in the callback and reads it on every call; the app
|
|
84
|
+
never passes it anywhere.
|
|
79
85
|
|
|
80
86
|
## 4 - The two routes
|
|
81
87
|
|
|
@@ -120,33 +126,7 @@ Vue: `@withone/connect/vue`, same props. Svelte: `use:connectButton` from
|
|
|
120
126
|
`<one-connect-button authorize-url="/api/one/authorize" platforms="gmail, stripe">`.
|
|
121
127
|
A custom button in React: `useOneConnect({ authorizeUrl })` returns `{ open, status }`.
|
|
122
128
|
|
|
123
|
-
## 6 -
|
|
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
|
-
|
|
149
|
-
## 7 - Calling One with the grant
|
|
129
|
+
## 6 - Calling One with the grant
|
|
150
130
|
|
|
151
131
|
```ts
|
|
152
132
|
const connections = await oneConnect.listConnections(userId); // [{ key, platform, access }]
|
|
@@ -159,20 +139,21 @@ const reply = await oneConnect.runAction(userId, {
|
|
|
159
139
|
path: action.path,
|
|
160
140
|
body: payload,
|
|
161
141
|
});
|
|
162
|
-
// { status, ok, data }
|
|
142
|
+
// { status, ok, blockedByGrant, data }
|
|
163
143
|
```
|
|
164
144
|
|
|
165
|
-
Do not set any header. The package adds the user's
|
|
166
|
-
|
|
145
|
+
Do not set any header. The package adds the connect key and the user's id
|
|
146
|
+
to every call it makes.
|
|
167
147
|
|
|
168
|
-
A `403` reply means the call is outside what
|
|
148
|
+
A `403` reply with `blockedByGrant: true` means the call is outside what
|
|
149
|
+
the user granted. Do not retry it.
|
|
169
150
|
|
|
170
151
|
Errors are `OneConnectError` with a `code`. Handle them where the app calls One:
|
|
171
152
|
|
|
172
153
|
| `code` | Meaning | Do |
|
|
173
154
|
|---|---|---|
|
|
174
155
|
| `not_connected` | Nothing is stored for this user. | Show the Connect button. |
|
|
175
|
-
| `
|
|
156
|
+
| `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. |
|
|
176
157
|
| `request_failed` | One answered with an error or could not be reached. Nothing stored changed. | Retry later. |
|
|
177
158
|
|
|
178
159
|
```ts
|
|
@@ -183,7 +164,7 @@ try {
|
|
|
183
164
|
} catch (error) {
|
|
184
165
|
if (
|
|
185
166
|
error instanceof OneConnectError &&
|
|
186
|
-
["not_connected", "
|
|
167
|
+
["not_connected", "reconnect_required"].includes(error.code)
|
|
187
168
|
) {
|
|
188
169
|
// show the Connect button again
|
|
189
170
|
} else {
|
|
@@ -192,31 +173,38 @@ try {
|
|
|
192
173
|
}
|
|
193
174
|
```
|
|
194
175
|
|
|
195
|
-
|
|
176
|
+
`isConnected(userId)` says the user has connected before. One confirms the
|
|
177
|
+
consent on each call, so a user who revoked is found by the next call
|
|
178
|
+
throwing `reconnect_required`.
|
|
196
179
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
-
|
|
200
|
-
|
|
180
|
+
## 7 - Rules
|
|
181
|
+
|
|
182
|
+
- Never put the client secret or the connect key in browser code, logs,
|
|
183
|
+
error reports, source files or prompts. Environment variables only.
|
|
184
|
+
- Connect keys work in Production only; the human creates the key with
|
|
185
|
+
the dashboard on Production.
|
|
186
|
+
- Store the per-user string as given.
|
|
201
187
|
- The registered redirect URI and `ONE_REDIRECT_URI` must be identical.
|
|
202
188
|
- Do not build a completion page; the callback redirect is the completion.
|
|
203
189
|
- Do not write OAuth steps or One request headers by hand; use the package.
|
|
204
190
|
- Do not switch an existing app from one mode to the other unless asked.
|
|
205
191
|
|
|
206
|
-
##
|
|
192
|
+
## 8 - Done when
|
|
207
193
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
194
|
+
1. The button leads to One's page; after signing in and authorizing, the user
|
|
195
|
+
lands back in the app and `onSuccess` fires.
|
|
196
|
+
2. One string is saved for the user.
|
|
197
|
+
3. `listConnections` returns only the granted connections.
|
|
198
|
+
4. `runAction` works for an action inside the grant and returns `403` with
|
|
199
|
+
`blockedByGrant: true` for one outside it.
|
|
200
|
+
5. After the user revokes the app in their One dashboard, the next call
|
|
201
|
+
fails with `reconnect_required` and the app asks them to connect again.
|
|
211
202
|
|
|
212
|
-
|
|
213
|
-
actions it runs.
|
|
214
|
-
- It works in Production only. The human creates the key with the dashboard
|
|
215
|
-
on Production.
|
|
203
|
+
## 9 - Token mode (only when asked)
|
|
216
204
|
|
|
217
|
-
The
|
|
218
|
-
|
|
219
|
-
|
|
205
|
+
The other way to hold the grant: the app stores an access token and a
|
|
206
|
+
refresh token per user, as a standard OAuth client. The routes, the button
|
|
207
|
+
and the calls are the same. No `ONE_CONNECT_KEY` is needed.
|
|
220
208
|
|
|
221
209
|
```ts
|
|
222
210
|
export const oneConnect = createOneConnect({
|
|
@@ -224,33 +212,44 @@ export const oneConnect = createOneConnect({
|
|
|
224
212
|
clientSecret: process.env.ONE_CLIENT_SECRET!,
|
|
225
213
|
redirectUri: process.env.ONE_REDIRECT_URI!,
|
|
226
214
|
permissionSet: process.env.ONE_PERMISSION_SET,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
clearUser: (userId) => /* set it to null */,
|
|
215
|
+
tokenStore: {
|
|
216
|
+
saveTokens: (userId, tokens) => /* save in the app's database, encrypted */,
|
|
217
|
+
loadTokens: (userId) => /* read; null when never connected */,
|
|
218
|
+
clearTokens: (userId) => /* delete */,
|
|
232
219
|
},
|
|
233
220
|
});
|
|
234
221
|
```
|
|
235
222
|
|
|
236
|
-
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
-
|
|
244
|
-
|
|
223
|
+
- Store tokens encrypted, keyed by the app's user.
|
|
224
|
+
- If the app runs more than one server process or a background worker, also
|
|
225
|
+
add `withLock: (userId, run) => ...`, which runs `run()` while holding a
|
|
226
|
+
per-user lock all processes share (for example a Postgres advisory lock).
|
|
227
|
+
- When the user revokes access, the next refresh throws `refresh_failed`
|
|
228
|
+
(not `reconnect_required`) and the tokens are cleared. Ask them to connect
|
|
229
|
+
again.
|
|
230
|
+
- `blockedByGrant` stays `false` in token mode; treat any `403` as outside
|
|
231
|
+
the grant.
|
|
245
232
|
|
|
246
|
-
|
|
233
|
+
Tokens expire, and two things keep them fresh. Only the first is automatic:
|
|
247
234
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
235
|
+
| | Who does it | When |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| Refresh before a call | the package, on its own | whenever the app calls One and the token is about to expire |
|
|
238
|
+
| Refresh for users who have not called in a while | **the app**, with a daily job | once a day, for every connected user |
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
// a scheduled job, once a day
|
|
242
|
+
for (const userId of /* every user with stored tokens */) {
|
|
243
|
+
await oneConnect.refreshIfExpiring(userId, { withinMs: 3 * 24 * 3_600_000 });
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Without the job, a user who stays away for 30 days has to connect again:
|
|
248
|
+
the refresh token lives 30 days, and only a live refresh token can renew
|
|
249
|
+
the pair. The access token lives as long as the app's Token lifetime says
|
|
250
|
+
(30 days unless changed under Advanced when creating the app); keep the
|
|
251
|
+
window shorter than that, or every run refreshes. In token mode, "done"
|
|
252
|
+
also means the daily job exists and runs for every connected user.
|
|
253
|
+
|
|
254
|
+
The mode is whichever credential `createOneConnect` is given: `connectKey`
|
|
255
|
+
and `userStore` for key mode, `tokenStore` for token mode.
|