@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 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 one id for the user, redirects home
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 · Choose a mode
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
- **Key mode**: pass the connect key and a place to save one value per user.
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
- oneApiUrl: process.env.ONE_API_URL,
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
- To be explicit, add `mode: "key"` or `mode: "token"`. `oneConnect.mode` tells you which one is running. An app written before key mode existed passes only a `tokenStore`, so it keeps running in token mode with no change.
120
+ Store the tokens in your database, encrypted, keyed by your user id.
156
121
 
157
- ## 4 · The two routes
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
- The same file in both modes:
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, blockedByGrant, data }
150
+ // { status, ok, data }
188
151
  ```
189
152
 
190
- A `403` means the call is outside what the user granted. Don't retry it. In key mode `blockedByGrant` is `true` for those.
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` | Mode | What it means | What to do |
195
- |---|---|---|---|
196
- | `not_connected` | both | Nothing is stored for this user. | Show the Connect button. |
197
- | `reconnect_required` | key | One will not act for this user: they revoked access, or the app is deactivated. | Ask the user to connect again. Your stored value is kept. |
198
- | `refresh_failed` | token | The grant ended (revoked or expired). The tokens were cleared. | Ask the user to connect again. |
199
- | `request_failed` | both | One answered with an error or could not be reached. Nothing stored changed. | Retry later. |
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
- ## 6 · Key mode notes
167
+ Tokens expire, and two things keep them fresh. One is automatic. The other is yours to run.
202
168
 
203
- - 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.
204
- - A key works in one environment. Use the Production key in production and the Sandbox key in sandbox; the wrong one refuses every user.
205
- - `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`.
206
- - `oneConnect.getConnectUserId(userId)` returns the user's One id (`cu_…`) if you want it for your own records.
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
- ## 7 · Token mode notes
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
- - Store the tokens in your database, encrypted, keyed by your user id. The SDK refreshes them for you.
212
- - 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. The refresh token lives 30 days, and only a live refresh token can renew the pair.
213
- - 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.
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.0",
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, choosing key mode or token mode, and calling One with the grant.
3
+ description: Add One Connect to an application so its users can grant the app scoped, revocable access to their own One-connected tools (Gmail, Slack, Notion, Stripe and 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 three things: a button, two routes, and the calls made with the grant.
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
- saves one id for the user, redirects home
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
- The client secret and the connect key stay on the server.
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 or the connect key into the chat.
37
- Tell them which environment variable to set and read it from there.
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
- `mode: "key"` or `mode: "token"` may be added to be explicit.
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
- In token mode, if the app runs more than one server process or a background
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
- In both modes use the app's own user id as the key and the database it
142
- already has.
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
- ## 5 - The two routes
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
- ## 6 - The button
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, blockedByGrant, data }
162
+ // { status, ok, data }
201
163
  ```
202
164
 
203
- A `403` reply means the call is outside what the user granted. Do not retry
204
- it. In key mode `blockedByGrant` is `true` for those.
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` | Mode | Meaning | Do |
209
- |---|---|---|---|
210
- | `not_connected` | both | Nothing is stored for this user. | Show the Connect button. |
211
- | `reconnect_required` | key | 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. |
212
- | `refresh_failed` | token | The grant ended. The tokens were cleared. | Ask the user to connect again. |
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", "reconnect_required", "refresh_failed"].includes(error.code)
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 or the connect key in browser code, logs,
239
- error reports, source files or prompts. Environment variables only.
240
- - A connect key works in one environment. Use the Production key against
241
- production and the Sandbox key against sandbox.
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 - Done when
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. Key mode: one string is saved for the user. Token mode: the tokens are saved.
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. After the user revokes the app in their One dashboard, the next call
258
- fails with `reconnect_required` (key mode) or `refresh_failed` (token
259
- mode) and the app asks them to connect again.
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.