@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 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 the grant for the user, redirects home
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 keeps an access token and a refresh token for each user who connects. This is **token mode**, and it is the one to use.
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
- tokenStore: {
113
- saveTokens: (userId, tokens) => db.oneTokens.upsert(userId, tokens),
114
- loadTokens: (userId) => db.oneTokens.find(userId),
115
- clearTokens: (userId) => db.oneTokens.delete(userId),
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
- Store the tokens in your database, encrypted, keyed by your user id.
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 credential to every call it makes.
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
- | `refresh_failed` | The grant ended (revoked or expired). The tokens were cleared. | Ask the user to connect again. |
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 · Keeping users connected
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
- ```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
- ```
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 · 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.
177
+ ## 7 · Token mode
190
178
 
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`.
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
- 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 }),
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
- - `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.
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.1",
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, storing and refreshing the grant, and calling One with it.
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 four things: a button, two routes, a daily refresh job, and the calls
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
- stores the tokens, redirects home
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 **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.
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
- Secrets and tokens stay on the server.
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. Tell them which
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
- tokenStore: {
67
- saveTokens: (userId, tokens) => /* save in the app's database, encrypted */,
68
- loadTokens: (userId) => /* read; null when never connected */,
69
- clearTokens: (userId) => /* delete */,
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
- 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).
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 - 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
-
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 credential to every call
166
- it makes, and refreshes it first when it is about to expire.
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 the user granted. Do not retry it.
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
- | `refresh_failed` | The grant ended. The tokens were cleared. | Ask the user to connect again. |
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", "refresh_failed"].includes(error.code)
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
- ## 8 - Rules
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
- - 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.
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
- ## 9 - Key mode (only when asked)
192
+ ## 8 - Done when
207
193
 
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:
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
- - `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.
203
+ ## 9 - Token mode (only when asked)
216
204
 
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.
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
- 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 */,
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
- - `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.
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
- ## 10 - Done when
233
+ Tokens expire, and two things keep them fresh. Only the first is automatic:
247
234
 
248
- 1. The button leads to One's page; after signing in and authorizing, the user
249
- lands back in the app and `onSuccess` fires.
250
- 2. The tokens are saved for the user.
251
- 3. `listConnections` returns only the granted connections.
252
- 4. `runAction` works for an action inside the grant and returns `403` for
253
- one outside it.
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.
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.