@withone/connect 0.12.2 → 0.13.0
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 +71 -10
- package/dist/next.d.ts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/server/credential.d.ts +45 -0
- package/dist/server/index.cjs.js +441 -135
- package/dist/server/index.d.ts +42 -22
- package/dist/server/index.esm.js +440 -136
- package/dist/server/key.d.ts +32 -0
- package/dist/server/token.d.ts +33 -0
- package/dist/server/types.d.ts +76 -8
- package/package.json +1 -1
- package/skills/one-connect/SKILL.md +129 -23
- package/src/next.ts +2 -2
- package/src/node.ts +2 -2
- package/src/server/credential.ts +44 -0
- package/src/server/index.ts +197 -221
- package/src/server/key.ts +142 -0
- package/src/server/token.ts +235 -0
- package/src/server/types.ts +82 -7
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
|
-
|
|
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 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
|
+
|
|
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: the app's connect key
|
|
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
|
ONE_API_URL=https://api.withone.ai # optional: production when unset
|
|
@@ -97,7 +100,19 @@ Your own button in React: `const { open, status } = useOneConnect({ authorizeUrl
|
|
|
97
100
|
|
|
98
101
|
To match your design, set `--one-connect-font` and `--one-connect-radius`, or style `::part(button)`.
|
|
99
102
|
|
|
100
|
-
## 3 ·
|
|
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 |
|
|
114
|
+
|
|
115
|
+
**Key mode**: pass the connect key and a place to save one value per user.
|
|
101
116
|
|
|
102
117
|
```ts
|
|
103
118
|
// lib/one.ts
|
|
@@ -109,7 +124,27 @@ export const oneConnect = createOneConnect({
|
|
|
109
124
|
redirectUri: process.env.ONE_REDIRECT_URI!,
|
|
110
125
|
permissionSet: process.env.ONE_PERMISSION_SET,
|
|
111
126
|
oneApiUrl: process.env.ONE_API_URL,
|
|
112
|
-
|
|
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
|
|
113
148
|
saveTokens: (userId, tokens) => db.oneTokens.upsert(userId, tokens),
|
|
114
149
|
loadTokens: (userId) => db.oneTokens.find(userId),
|
|
115
150
|
clearTokens: (userId) => db.oneTokens.delete(userId),
|
|
@@ -117,6 +152,12 @@ export const oneConnect = createOneConnect({
|
|
|
117
152
|
});
|
|
118
153
|
```
|
|
119
154
|
|
|
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.
|
|
156
|
+
|
|
157
|
+
## 4 · The two routes
|
|
158
|
+
|
|
159
|
+
The same file in both modes:
|
|
160
|
+
|
|
120
161
|
```ts
|
|
121
162
|
// app/api/one/[action]/route.ts (Next.js; also Remix, SvelteKit, Hono, Bun)
|
|
122
163
|
import { createOneConnectRoutes } from "@withone/connect/next";
|
|
@@ -129,7 +170,9 @@ export const { GET } = createOneConnectRoutes(oneConnect, {
|
|
|
129
170
|
|
|
130
171
|
That file serves `/api/one/authorize` and `/api/one/callback`. For Express or plain Node, use `createOneConnectHandlers` from `@withone/connect/node`.
|
|
131
172
|
|
|
132
|
-
##
|
|
173
|
+
## 5 · Using the grant
|
|
174
|
+
|
|
175
|
+
The same calls in both modes:
|
|
133
176
|
|
|
134
177
|
```ts
|
|
135
178
|
const connections = await oneConnect.listConnections(userId); // what the user granted
|
|
@@ -141,17 +184,35 @@ const reply = await oneConnect.runAction(userId, {
|
|
|
141
184
|
method: actions[0].method,
|
|
142
185
|
path: actions[0].path,
|
|
143
186
|
});
|
|
144
|
-
// { status, ok, data }
|
|
187
|
+
// { status, ok, blockedByGrant, data }
|
|
145
188
|
```
|
|
146
189
|
|
|
147
|
-
A `403` means the call is outside what the user granted. Don't retry it.
|
|
190
|
+
A `403` means the call is outside what the user granted. Don't retry it. In key mode `blockedByGrant` is `true` for those.
|
|
191
|
+
|
|
192
|
+
Errors are `OneConnectError` with a `code`:
|
|
193
|
+
|
|
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. |
|
|
200
|
+
|
|
201
|
+
## 6 · Key mode notes
|
|
202
|
+
|
|
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**.
|
|
148
208
|
|
|
149
|
-
##
|
|
209
|
+
## 7 · Token mode notes
|
|
150
210
|
|
|
151
|
-
- Store
|
|
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.
|
|
152
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.
|
|
153
|
-
-
|
|
154
|
-
-
|
|
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.
|
|
155
216
|
|
|
156
217
|
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
218
|
|
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 {
|
|
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:
|
|
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 {
|
|
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:
|
|
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
|
+
}
|