@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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Key mode: the app holds one connect key, and one permanent id per
3
+ * user. Both go on every call; One reads the user's consent live each
4
+ * time. Nothing expires, so there is nothing to refresh, rotate or lock.
5
+ *
6
+ * X-One-Secret: the app's connect key
7
+ * X-One-Connect-User-Id: cu_… for the user being acted for
8
+ *
9
+ * The id comes from the one code exchange the callback makes. It is the
10
+ * same for a user for the life of the app, through revocation and
11
+ * re-consent, so it is saved once and never deleted by the SDK.
12
+ */
13
+ import type { Credential } from "./credential";
14
+ import { type OneConnectUserReference, type OneConnectUserStore } from "./types";
15
+ /**
16
+ * The one value an app stores per user, as a string for one column:
17
+ * `cu_…`, then the space the user granted from when it is not their
18
+ * personal one (`cu_…;org=<id>;project=<id>`).
19
+ *
20
+ * The space travels with the id because One resolves a call's tenant
21
+ * from headers: a grant made from an organization lives there, and a
22
+ * call that names none runs in the user's personal space and reaches
23
+ * nothing. In token mode the access token names the space on every
24
+ * call; here there is no token after the exchange, so it is kept.
25
+ */
26
+ export declare function encodeUserReference(reference: OneConnectUserReference): string;
27
+ /** Reads a stored value back. Null when it is not one this SDK wrote. */
28
+ export declare function parseUserReference(value: string): OneConnectUserReference | null;
29
+ export interface KeyCredential extends Credential {
30
+ getConnectUserId: (userId: string) => Promise<string | null>;
31
+ }
32
+ export declare function createKeyCredential(connectKey: string, userStore: OneConnectUserStore): KeyCredential;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Token mode: the app holds an access token and a refresh token per
3
+ * user, and the SDK refreshes them with One.
4
+ *
5
+ * Lifetimes. The access token lives as long as the app's Token lifetime
6
+ * setting says (dashboard, Advanced, when creating or editing the app: 7
7
+ * days, 30 days, 90 days or 1 year; 30 days unless changed). An app that
8
+ * never had the setting gets an hour. The refresh token always lives 30
9
+ * days, whatever the access token's lifetime, and only a live refresh
10
+ * token can buy a new pair.
11
+ *
12
+ * Who refreshes. Before each call the client refreshes a pair that is
13
+ * within a minute of expiring. Refreshing earlier than that is the app's
14
+ * job: it runs `refreshIfExpiring` on a schedule, so the refresh token
15
+ * never runs out. When it has run out, the access token is used until it
16
+ * expires too, and only then is the user asked to connect again.
17
+ *
18
+ * Rotation. Every refresh rotates both tokens, and One treats a second
19
+ * use of a rotated refresh token as theft and revokes the whole grant.
20
+ * So the client refreshes one user at a time (in this process always,
21
+ * across processes through `tokenStore.withLock`), re-reads the store
22
+ * before spending a refresh token, clears tokens only when the grant is
23
+ * dead, and never lets a failing old pair delete a newer one.
24
+ */
25
+ import type { Credential, PostToken } from "./credential";
26
+ import { type OneConnectTokenStore, type OneConnectTokens, type RefreshIfExpiringOptions } from "./types";
27
+ export interface TokenCredential extends Credential {
28
+ getAccessToken: (userId: string) => Promise<string>;
29
+ getTokens: (userId: string) => Promise<OneConnectTokens | null>;
30
+ refreshTokens: (userId: string) => Promise<OneConnectTokens>;
31
+ refreshIfExpiring: (userId: string, options?: RefreshIfExpiringOptions) => Promise<OneConnectTokens>;
32
+ }
33
+ export declare function createTokenCredential(tokenStore: OneConnectTokenStore, postToken: PostToken): TokenCredential;
@@ -2,6 +2,39 @@
2
2
  * Types for `@withone/connect/server`: the half of the SDK that runs on
3
3
  * the app's server and holds the client secret.
4
4
  */
5
+ /**
6
+ * How the app holds a user's grant.
7
+ *
8
+ * - `"key"`: the app's connect key plus a permanent id per user. Nothing
9
+ * expires, so there is nothing to refresh. The default.
10
+ * - `"token"`: an access token and a refresh token per user, which the
11
+ * SDK keeps fresh.
12
+ */
13
+ export type OneConnectMode = "key" | "token";
14
+ /** Key mode: who a stored user is to One, and where their grant lives. */
15
+ export interface OneConnectUserReference {
16
+ /** One's permanent id for this user, for this app: `cu_…`. */
17
+ connectUserId: string;
18
+ /** The organization the user granted from; absent for their personal
19
+ * space. */
20
+ organizationId?: string;
21
+ /** The project the user granted from, when it was one. */
22
+ projectId?: string;
23
+ }
24
+ /**
25
+ * Key mode: where the app keeps the one value the SDK gives it per user.
26
+ * A single string, written when the user connects and the same until
27
+ * they connect again, so one column on the app's user row is enough.
28
+ * `userId` is the app's own id for its user.
29
+ *
30
+ * It is an identifier, not a credential: it does nothing without the
31
+ * app's connect key. No lock is needed, because nothing rotates.
32
+ */
33
+ export interface OneConnectUserStore {
34
+ saveUser: (userId: string, reference: string) => Promise<void>;
35
+ loadUser: (userId: string) => Promise<string | null>;
36
+ clearUser: (userId: string) => Promise<void>;
37
+ }
5
38
  /** What the app stores per user after the exchange. */
6
39
  export interface OneConnectTokens {
7
40
  accessToken: string;
@@ -47,10 +80,13 @@ export interface OneConnectTokenStore {
47
80
  }
48
81
  export interface RefreshIfExpiringOptions {
49
82
  /** Refresh when the access token or the refresh token expires within
50
- * this many milliseconds. One minute when omitted. */
83
+ * this many milliseconds. One minute when omitted. A refresh token
84
+ * that has already run out cannot be refreshed: the pair is returned
85
+ * as it is while its access token still works. */
51
86
  withinMs?: number;
52
87
  }
53
- export interface OneConnectServerConfig {
88
+ /** What every app configures, whichever mode it uses. */
89
+ export interface OneConnectBaseConfig {
54
90
  /** The app's client id from the dashboard. */
55
91
  clientId: string;
56
92
  /** The app's client secret. Server only. */
@@ -69,8 +105,35 @@ export interface OneConnectServerConfig {
69
105
  /** OAuth scopes. All three tenancy tiers when omitted, so the user may
70
106
  * grant from any space. */
71
107
  scopes?: string[];
108
+ }
109
+ /**
110
+ * Key mode, the default: pass the app's connect key and a place to keep
111
+ * one value per user.
112
+ */
113
+ export interface OneConnectKeyConfig extends OneConnectBaseConfig {
114
+ mode?: "key";
115
+ /** The app's connect key, minted on the app's page in the dashboard.
116
+ * Server only. One key per environment. */
117
+ connectKey: string;
118
+ userStore: OneConnectUserStore;
119
+ tokenStore?: never;
120
+ }
121
+ /**
122
+ * Token mode: pass a token store and the SDK keeps each user's tokens
123
+ * fresh.
124
+ */
125
+ export interface OneConnectTokenConfig extends OneConnectBaseConfig {
126
+ mode?: "token";
72
127
  tokenStore: OneConnectTokenStore;
128
+ connectKey?: never;
129
+ userStore?: never;
73
130
  }
131
+ /**
132
+ * The mode is whichever credential is configured: a `connectKey` is key
133
+ * mode, a `tokenStore` alone is token mode. Set `mode` to say so
134
+ * explicitly.
135
+ */
136
+ export type OneConnectServerConfig = OneConnectKeyConfig | OneConnectTokenConfig;
74
137
  /** The transaction cookie the authorize leg sets and the callback reads. */
75
138
  export interface OneConnectCookie {
76
139
  name: string;
@@ -160,18 +223,23 @@ export interface RunActionResult {
160
223
  status: number;
161
224
  ok: boolean;
162
225
  /** True when One refused the call because it is outside the grant.
163
- * The provider was never called. Do not retry. */
226
+ * The provider was never called. Do not retry. Key mode sets it; in
227
+ * token mode it stays false today, so treat any 403 there as refused. */
164
228
  blockedByGrant: boolean;
165
229
  data: unknown;
166
230
  }
167
231
  /**
168
- * - `not_connected`: no tokens are stored for this user.
169
- * - `refresh_failed`: One declared the grant dead (revoked, expired or
170
- * reused). The tokens were cleared; ask the user to connect again.
232
+ * - `not_connected`: nothing is stored for this user.
233
+ * - `reconnect_required` (key mode): One will not act for this user.
234
+ * Their consent was revoked, or the app is deactivated. What the app
235
+ * stored is kept; ask the user to connect again.
236
+ * - `refresh_failed` (token mode): One declared the grant dead (revoked,
237
+ * expired or reused). The tokens were cleared; ask the user to connect
238
+ * again.
171
239
  * - `request_failed`: One answered with an error or could not be
172
- * reached. During a refresh the tokens are kept, so retry later.
240
+ * reached. Nothing stored was changed, so retry later.
173
241
  */
174
- export type OneConnectErrorCode = "not_connected" | "refresh_failed" | "request_failed";
242
+ export type OneConnectErrorCode = "not_connected" | "reconnect_required" | "refresh_failed" | "request_failed";
175
243
  export declare class OneConnectError extends Error {
176
244
  readonly code: OneConnectErrorCode;
177
245
  readonly status?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/connect",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
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,6 +1,6 @@
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, 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, choosing key mode or token mode, and calling One with the grant.
4
4
  ---
5
5
 
6
6
  # One Connect
@@ -13,12 +13,12 @@ you wire three things: a button, two routes, and the calls made with the grant.
13
13
  Browser Your backend One
14
14
  <ConnectButton> ------> GET /api/one/authorize ---302---> hosted page: sign in, pick tools, set access
15
15
  GET /api/one/callback <--302---- ?code&state
16
- stores the tokens, redirects home
16
+ saves one id for the user, redirects home
17
17
  <------ onSuccess fires
18
18
  Later: oneConnect.runAction(userId, ...) -> One, grant enforced
19
19
  ```
20
20
 
21
- Secrets and tokens stay on the server.
21
+ The client secret and the connect key stay on the server.
22
22
 
23
23
  ## 1 - Ask the human for these
24
24
 
@@ -28,30 +28,91 @@ They create the app in the One dashboard: Developers -> Connect -> New app.
28
28
  |---|---|
29
29
  | `ONE_CLIENT_ID` | From the app. |
30
30
  | `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). |
31
32
  | Redirect URI | Registered on the app. Must match the callback route exactly, e.g. `http://localhost:3000/api/one/callback`. |
32
33
  | `ONE_PERMISSION_SET` | Optional. The tools and access levels to ask for. |
33
34
  | `ONE_API_URL` | Optional. Production when unset; `https://development-api.withone.ai` for the development dashboard. |
34
35
 
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.
38
+
35
39
  ## 2 - Environment (server only)
36
40
 
37
41
  ```bash
38
42
  ONE_CLIENT_ID=...
39
43
  ONE_CLIENT_SECRET=one_secret_...
44
+ ONE_CONNECT_KEY=sk_live_... # key mode
40
45
  ONE_REDIRECT_URI=https://yourapp.com/api/one/callback
41
46
  ONE_PERMISSION_SET=... # optional
42
47
  ONE_API_URL=... # optional
43
48
  ```
44
49
 
45
- ## 3 - Install and create the client
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
+ ```
77
+
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
46
82
 
47
83
  ```bash
48
84
  npm install @withone/connect
49
85
  ```
50
86
 
87
+ Key mode:
88
+
51
89
  ```ts
52
90
  // lib/one.ts (server only)
53
91
  import { createOneConnect } from "@withone/connect/server";
54
92
 
93
+ export const oneConnect = createOneConnect({
94
+ clientId: process.env.ONE_CLIENT_ID!,
95
+ clientSecret: process.env.ONE_CLIENT_SECRET!,
96
+ redirectUri: process.env.ONE_REDIRECT_URI!,
97
+ 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
55
116
  export const oneConnect = createOneConnect({
56
117
  clientId: process.env.ONE_CLIENT_ID!,
57
118
  clientSecret: process.env.ONE_CLIENT_SECRET!,
@@ -66,13 +127,21 @@ export const oneConnect = createOneConnect({
66
127
  });
67
128
  ```
68
129
 
69
- Use the app's own user id as the key and the database it already has.
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.
70
140
 
71
- If the app runs more than one server process or a background worker, also
72
- add `withLock: (userId, run) => ...`, which runs `run()` while holding a
73
- per-user lock all processes share (for example a Postgres advisory lock).
141
+ In both modes use the app's own user id as the key and the database it
142
+ already has.
74
143
 
75
- ## 4 - The two routes
144
+ ## 5 - The two routes
76
145
 
77
146
  Next.js App Router (also Remix, SvelteKit, Hono, Bun):
78
147
 
@@ -92,7 +161,7 @@ Express or plain Node: `createOneConnectHandlers(oneConnect, { identifyUser })`
92
161
  from `@withone/connect/node`, mounted at `/api/one/authorize` and
93
162
  `/api/one/callback`.
94
163
 
95
- ## 5 - The button
164
+ ## 6 - The button
96
165
 
97
166
  ```tsx
98
167
  import { ConnectButton } from "@withone/connect/react";
@@ -115,7 +184,7 @@ Vue: `@withone/connect/vue`, same props. Svelte: `use:connectButton` from
115
184
  `<one-connect-button authorize-url="/api/one/authorize" platforms="gmail, stripe">`.
116
185
  A custom button in React: `useOneConnect({ authorizeUrl })` returns `{ open, status }`.
117
186
 
118
- ## 6 - Calling One with the grant
187
+ ## 7 - Calling One with the grant
119
188
 
120
189
  ```ts
121
190
  const connections = await oneConnect.listConnections(userId); // [{ key, platform, access }]
@@ -128,26 +197,63 @@ const reply = await oneConnect.runAction(userId, {
128
197
  path: action.path,
129
198
  body: payload,
130
199
  });
131
- // { status, ok, data }
200
+ // { status, ok, blockedByGrant, data }
201
+ ```
202
+
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.
205
+
206
+ Errors are `OneConnectError` with a `code`. Handle them where the app calls One:
207
+
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. |
214
+
215
+ ```ts
216
+ import { OneConnectError } from "@withone/connect/server";
217
+
218
+ try {
219
+ await oneConnect.runAction(userId, action);
220
+ } catch (error) {
221
+ if (
222
+ error instanceof OneConnectError &&
223
+ ["not_connected", "reconnect_required", "refresh_failed"].includes(error.code)
224
+ ) {
225
+ // show the Connect button again
226
+ } else {
227
+ throw error;
228
+ }
229
+ }
132
230
  ```
133
231
 
134
- A `403` means the call is outside what the user granted. Do not retry it.
135
- A `refresh_failed` error means the grant ended; ask the user to connect again.
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`.
136
235
 
137
- ## 7 - Rules
236
+ ## 8 - Rules
138
237
 
139
- - Never put the client secret in browser code, logs or error reports.
140
- - Store tokens encrypted, keyed by the app's user.
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.
141
244
  - The registered redirect URI and `ONE_REDIRECT_URI` must be identical.
142
245
  - Do not build a completion page; the callback redirect is the completion.
143
- - Do not write OAuth steps by hand; use the package.
246
+ - Do not write OAuth steps or One request headers by hand; use the package.
247
+ - Do not switch an existing app from one mode to the other unless asked.
144
248
 
145
- ## 8 - Done when
249
+ ## 9 - Done when
146
250
 
147
251
  1. The button leads to One's page; after signing in and authorizing, the user
148
252
  lands back in the app and `onSuccess` fires.
149
- 2. `listConnections` returns only the granted connections.
150
- 3. `runAction` works for an action inside the grant and returns `403` for
253
+ 2. Key mode: one string is saved for the user. Token mode: the tokens are saved.
254
+ 3. `listConnections` returns only the granted connections.
255
+ 4. `runAction` works for an action inside the grant and returns `403` for
151
256
  one outside it.
152
- 4. After the user revokes the app in their One dashboard, the app asks them
153
- to connect again.
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.
package/src/next.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
 
19
19
  export interface OneConnectRoutesOptions {
20
20
  /** The app's own id for the signed-in user, or null when nobody is
@@ -80,7 +80,7 @@ export function routeFor(url: string): string {
80
80
  }
81
81
 
82
82
  export function createOneConnectRoutes(
83
- oneConnect: OneConnect,
83
+ oneConnect: Pick<OneConnectClient, "startAuthorization" | "completeAuthorization">,
84
84
  options: OneConnectRoutesOptions,
85
85
  ): OneConnectRoutes {
86
86
  const cookiePath = (): string =>
package/src/node.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
18
18
 
19
19
  import { createOneConnectRoutes, type OneConnectRoutesOptions } from "@withone/connect/next";
20
- import type { OneConnect } from "./server";
20
+ import type { OneConnectClient } from "./server";
21
21
 
22
22
  export interface OneConnectNodeOptions {
23
23
  /** The app's own id for the signed-in user, or null when nobody is
@@ -76,7 +76,7 @@ async function send(response: ServerResponse, web: Response): Promise<void> {
76
76
  }
77
77
 
78
78
  export function createOneConnectHandlers(
79
- oneConnect: OneConnect,
79
+ oneConnect: Pick<OneConnectClient, "startAuthorization" | "completeAuthorization">,
80
80
  options: OneConnectNodeOptions,
81
81
  ): OneConnectHandlers {
82
82
  // The web adapter receives a Request; the Node callbacks want the
@@ -0,0 +1,44 @@
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
+
16
+ /** What One's token endpoint answers on success. `connect_user_id` is
17
+ * present whenever a durable grant backs the token. */
18
+ export interface TokenResponse {
19
+ access_token: string;
20
+ refresh_token: string;
21
+ expires_in: number;
22
+ connect_user_id?: string;
23
+ }
24
+
25
+ /** What One's token endpoint answered. A network failure throws instead. */
26
+ export type TokenAnswer =
27
+ | { ok: true; body: TokenResponse }
28
+ | { ok: false; status: number; error?: string };
29
+
30
+ export type PostToken = (body: URLSearchParams) => Promise<TokenAnswer>;
31
+
32
+ export interface Credential {
33
+ /** The callback exchanged the code: keep what this mode needs. */
34
+ connected: (userId: string, response: TokenResponse) => Promise<void>;
35
+ /** The headers that make a /v1 call act for this user. Throws
36
+ * `not_connected` when the user has nothing stored. */
37
+ headers: (userId: string) => Promise<Record<string, string>>;
38
+ isConnected: (userId: string) => Promise<boolean>;
39
+ /** Drops the app's copy. The user revokes the grant itself in One. */
40
+ disconnect: (userId: string) => Promise<void>;
41
+ /** One refused the credential itself, rather than the call it carried:
42
+ * the error to throw, or null when this answer is not that. */
43
+ refusal: (status: number, body: string) => OneConnectError | null;
44
+ }