@withone/connect 0.12.2 → 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
- stores the tokens, 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
  ```
@@ -50,7 +50,6 @@ ONE_CLIENT_ID=…
50
50
  ONE_CLIENT_SECRET=one_secret_… # server only
51
51
  ONE_REDIRECT_URI=https://yourapp.com/api/one/callback # exactly the registered URL
52
52
  ONE_PERMISSION_SET=… # optional: the tools you ask for
53
- ONE_API_URL=https://api.withone.ai # optional: production when unset
54
53
  ```
55
54
 
56
55
  ## 2 · The button
@@ -97,7 +96,9 @@ Your own button in React: `const { open, status } = useOneConnect({ authorizeUrl
97
96
 
98
97
  To match your design, set `--one-connect-font` and `--one-connect-radius`, or style `::part(button)`.
99
98
 
100
- ## 3 · The two routes
99
+ ## 3 · The server client
100
+
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.
101
102
 
102
103
  ```ts
103
104
  // lib/one.ts
@@ -108,7 +109,6 @@ export const oneConnect = createOneConnect({
108
109
  clientSecret: process.env.ONE_CLIENT_SECRET!,
109
110
  redirectUri: process.env.ONE_REDIRECT_URI!,
110
111
  permissionSet: process.env.ONE_PERMISSION_SET,
111
- oneApiUrl: process.env.ONE_API_URL,
112
112
  tokenStore: {
113
113
  saveTokens: (userId, tokens) => db.oneTokens.upsert(userId, tokens),
114
114
  loadTokens: (userId) => db.oneTokens.find(userId),
@@ -117,6 +117,12 @@ export const oneConnect = createOneConnect({
117
117
  });
118
118
  ```
119
119
 
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.
123
+
124
+ ## 4 · The two routes
125
+
120
126
  ```ts
121
127
  // app/api/one/[action]/route.ts (Next.js; also Remix, SvelteKit, Hono, Bun)
122
128
  import { createOneConnectRoutes } from "@withone/connect/next";
@@ -129,7 +135,7 @@ export const { GET } = createOneConnectRoutes(oneConnect, {
129
135
 
130
136
  That file serves `/api/one/authorize` and `/api/one/callback`. For Express or plain Node, use `createOneConnectHandlers` from `@withone/connect/node`.
131
137
 
132
- ## 4 · Using the grant
138
+ ## 5 · Using the grant
133
139
 
134
140
  ```ts
135
141
  const connections = await oneConnect.listConnections(userId); // what the user granted
@@ -144,17 +150,71 @@ const reply = await oneConnect.runAction(userId, {
144
150
  // { status, ok, data }
145
151
  ```
146
152
 
153
+ You never set a header: the client adds the user's credential to every call it makes.
154
+
147
155
  A `403` means the call is outside what the user granted. Don't retry it.
148
156
 
149
- ## 5 · Tokens
157
+ Errors are `OneConnectError` with a `code`:
158
+
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
166
+
167
+ Tokens expire, and two things keep them fresh. One is automatic. The other is yours to run.
150
168
 
151
- - Store them in your database, encrypted, keyed by your user id. The SDK refreshes them for you.
152
- - 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.
153
- - Keep idle users connected: once a day, call `oneConnect.refreshIfExpiring(userId, { withinMs: 7 * 24 * 3_600_000 })`.
154
- - A `refresh_failed` error means the grant ended (revoked or expired). Ask the user to connect again.
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 |
173
+
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.
155
184
 
156
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.
157
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
+
158
218
  ## License
159
219
 
160
220
  GPL-3.0. See [LICENSE](LICENSE).
package/dist/next.d.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * That serves /api/one/authorize and /api/one/callback. Register
15
15
  * `https://yourapp.com/api/one/callback` as the app's redirect URI.
16
16
  */
17
- import type { OneConnect } from "./server";
17
+ import type { OneConnectClient } from "./server";
18
18
  export interface OneConnectRoutesOptions {
19
19
  /** The app's own id for the signed-in user, or null when nobody is
20
20
  * signed in. The grant is stored under this id. */
@@ -33,4 +33,4 @@ export declare const CALLBACK_ROUTE = "callback";
33
33
  export declare function readCookie(request: Request, name: string): string | undefined;
34
34
  /** The last path segment names the leg: ".../authorize" or ".../callback". */
35
35
  export declare function routeFor(url: string): string;
36
- export declare function createOneConnectRoutes(oneConnect: OneConnect, options: OneConnectRoutesOptions): OneConnectRoutes;
36
+ export declare function createOneConnectRoutes(oneConnect: Pick<OneConnectClient, "startAuthorization" | "completeAuthorization">, options: OneConnectRoutesOptions): OneConnectRoutes;
package/dist/node.d.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * app.get("/api/one/callback", callback);
16
16
  */
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
18
- import type { OneConnect } from "./server";
18
+ import type { OneConnectClient } from "./server";
19
19
  export interface OneConnectNodeOptions {
20
20
  /** The app's own id for the signed-in user, or null when nobody is
21
21
  * signed in. */
@@ -30,4 +30,4 @@ export interface OneConnectHandlers {
30
30
  authorize: NodeHandler;
31
31
  callback: NodeHandler;
32
32
  }
33
- export declare function createOneConnectHandlers(oneConnect: OneConnect, options: OneConnectNodeOptions): OneConnectHandlers;
33
+ export declare function createOneConnectHandlers(oneConnect: Pick<OneConnectClient, "startAuthorization" | "completeAuthorization">, options: OneConnectNodeOptions): OneConnectHandlers;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The seam between the two ways an app can hold a user's grant.
3
+ *
4
+ * Everything around it is shared: the authorize leg, the callback, and
5
+ * the calls made with the grant. A credential answers only what differs:
6
+ * what to keep when a user connects, what to send on a call, and how to
7
+ * read One refusing it.
8
+ *
9
+ * - key mode (`./key`): the app's connect key plus a permanent id for
10
+ * the user. Nothing expires, so nothing is refreshed.
11
+ * - token mode (`./token`): an access token and a refresh token per
12
+ * user, rotated before they expire.
13
+ */
14
+ import type { OneConnectError } from "./types";
15
+ /** What One's token endpoint answers on success. `connect_user_id` is
16
+ * present whenever a durable grant backs the token. */
17
+ export interface TokenResponse {
18
+ access_token: string;
19
+ refresh_token: string;
20
+ expires_in: number;
21
+ connect_user_id?: string;
22
+ }
23
+ /** What One's token endpoint answered. A network failure throws instead. */
24
+ export type TokenAnswer = {
25
+ ok: true;
26
+ body: TokenResponse;
27
+ } | {
28
+ ok: false;
29
+ status: number;
30
+ error?: string;
31
+ };
32
+ export type PostToken = (body: URLSearchParams) => Promise<TokenAnswer>;
33
+ export interface Credential {
34
+ /** The callback exchanged the code: keep what this mode needs. */
35
+ connected: (userId: string, response: TokenResponse) => Promise<void>;
36
+ /** The headers that make a /v1 call act for this user. Throws
37
+ * `not_connected` when the user has nothing stored. */
38
+ headers: (userId: string) => Promise<Record<string, string>>;
39
+ isConnected: (userId: string) => Promise<boolean>;
40
+ /** Drops the app's copy. The user revokes the grant itself in One. */
41
+ disconnect: (userId: string) => Promise<void>;
42
+ /** One refused the credential itself, rather than the call it carried:
43
+ * the error to throw, or null when this answer is not that. */
44
+ refusal: (status: number, body: string) => OneConnectError | null;
45
+ }