@withone/connect 0.10.0 → 0.12.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/dist/vue.d.ts CHANGED
@@ -1,35 +1,52 @@
1
1
  import type { PropType } from "vue";
2
- import { type ConnectButtonPlatformInput, type ConnectButtonVariant, type OneConnectTheme } from "@withone/connect";
2
+ import { type ConnectButtonPlatformInput, type ConnectButtonSize, type ConnectButtonTheme, type ConnectButtonVariant, type ConnectFailureCode, type OneConnectTheme } from "@withone/connect";
3
3
  export declare const ConnectButton: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
4
4
  authorizeUrl: {
5
5
  type: StringConstructor;
6
6
  required: true;
7
7
  };
8
- appTheme: {
9
- type: PropType<OneConnectTheme>;
8
+ platforms: {
9
+ type: PropType<ConnectButtonPlatformInput[]>;
10
10
  default: undefined;
11
11
  };
12
- label: {
13
- type: StringConstructor;
12
+ connected: {
13
+ type: BooleanConstructor;
14
+ default: undefined;
15
+ };
16
+ disabled: {
17
+ type: BooleanConstructor;
14
18
  default: undefined;
15
19
  };
16
20
  variant: {
17
21
  type: PropType<ConnectButtonVariant>;
18
22
  default: undefined;
19
23
  };
24
+ size: {
25
+ type: PropType<ConnectButtonSize>;
26
+ default: undefined;
27
+ };
28
+ fullWidth: {
29
+ type: BooleanConstructor;
30
+ default: undefined;
31
+ };
20
32
  theme: {
33
+ type: PropType<ConnectButtonTheme>;
34
+ default: undefined;
35
+ };
36
+ connectTheme: {
21
37
  type: PropType<OneConnectTheme>;
22
38
  default: undefined;
23
39
  };
24
- platforms: {
25
- type: PropType<ConnectButtonPlatformInput[]>;
40
+ /** @deprecated use connectTheme */
41
+ appTheme: {
42
+ type: PropType<OneConnectTheme>;
26
43
  default: undefined;
27
44
  };
28
- description: {
45
+ accentColor: {
29
46
  type: StringConstructor;
30
47
  default: undefined;
31
48
  };
32
- accentColor: {
49
+ label: {
33
50
  type: StringConstructor;
34
51
  default: undefined;
35
52
  };
@@ -37,41 +54,63 @@ export declare const ConnectButton: import("vue").DefineComponent<import("vue").
37
54
  type: StringConstructor;
38
55
  default: undefined;
39
56
  };
57
+ description: {
58
+ type: StringConstructor;
59
+ default: undefined;
60
+ };
40
61
  }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
41
62
  [key: string]: any;
42
63
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
43
64
  success: () => true;
44
- error: (message: string) => boolean;
65
+ error: (message: string, code: ConnectFailureCode) => boolean;
66
+ cancel: () => true;
45
67
  }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
46
68
  authorizeUrl: {
47
69
  type: StringConstructor;
48
70
  required: true;
49
71
  };
50
- appTheme: {
51
- type: PropType<OneConnectTheme>;
72
+ platforms: {
73
+ type: PropType<ConnectButtonPlatformInput[]>;
52
74
  default: undefined;
53
75
  };
54
- label: {
55
- type: StringConstructor;
76
+ connected: {
77
+ type: BooleanConstructor;
78
+ default: undefined;
79
+ };
80
+ disabled: {
81
+ type: BooleanConstructor;
56
82
  default: undefined;
57
83
  };
58
84
  variant: {
59
85
  type: PropType<ConnectButtonVariant>;
60
86
  default: undefined;
61
87
  };
88
+ size: {
89
+ type: PropType<ConnectButtonSize>;
90
+ default: undefined;
91
+ };
92
+ fullWidth: {
93
+ type: BooleanConstructor;
94
+ default: undefined;
95
+ };
62
96
  theme: {
97
+ type: PropType<ConnectButtonTheme>;
98
+ default: undefined;
99
+ };
100
+ connectTheme: {
63
101
  type: PropType<OneConnectTheme>;
64
102
  default: undefined;
65
103
  };
66
- platforms: {
67
- type: PropType<ConnectButtonPlatformInput[]>;
104
+ /** @deprecated use connectTheme */
105
+ appTheme: {
106
+ type: PropType<OneConnectTheme>;
68
107
  default: undefined;
69
108
  };
70
- description: {
109
+ accentColor: {
71
110
  type: StringConstructor;
72
111
  default: undefined;
73
112
  };
74
- accentColor: {
113
+ label: {
75
114
  type: StringConstructor;
76
115
  default: undefined;
77
116
  };
@@ -79,16 +118,26 @@ export declare const ConnectButton: import("vue").DefineComponent<import("vue").
79
118
  type: StringConstructor;
80
119
  default: undefined;
81
120
  };
121
+ description: {
122
+ type: StringConstructor;
123
+ default: undefined;
124
+ };
82
125
  }>> & Readonly<{
83
126
  onSuccess?: (() => any) | undefined;
84
- onError?: ((message: string) => any) | undefined;
127
+ onError?: ((message: string, code: ConnectFailureCode) => any) | undefined;
128
+ onCancel?: (() => any) | undefined;
85
129
  }>, {
130
+ connected: boolean;
86
131
  label: string;
87
- variant: ConnectButtonVariant;
88
- theme: OneConnectTheme;
132
+ disabled: boolean;
133
+ connectTheme: OneConnectTheme;
134
+ appTheme: OneConnectTheme;
89
135
  platforms: ConnectButtonPlatformInput[];
136
+ variant: ConnectButtonVariant;
137
+ size: ConnectButtonSize;
138
+ theme: ConnectButtonTheme;
90
139
  description: string;
91
- appTheme: OneConnectTheme;
140
+ fullWidth: boolean;
92
141
  accentColor: string;
93
142
  connectedLabel: string;
94
143
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
package/dist/vue.esm.js CHANGED
@@ -1 +1 @@
1
- import{defineComponent as e,ref as t,onMounted as r,watch as o,onBeforeUnmount as n,h as i}from"vue";import{propsIdentity as l,mountConnectButton as u,optionsFromProps as d}from"@withone/connect";const a=e({name:"OneConnectButton",props:{authorizeUrl:{type:String,required:!0},appTheme:{type:String,default:void 0},label:{type:String,default:void 0},variant:{type:String,default:void 0},theme:{type:String,default:void 0},platforms:{type:Array,default:void 0},description:{type:String,default:void 0},accentColor:{type:String,default:void 0},connectedLabel:{type:String,default:void 0}},emits:{success:()=>!0,error:e=>"string"==typeof e},setup(e,{emit:a}){const p=t(null);let s=null;const c=()=>{var t;null===(t=s)||void 0===t||t.destroy(),s=null,p.value&&(s=u(p.value,d({...e},{onSuccess:()=>a("success"),onError:e=>a("error",e)})))};return r(c),o(()=>l({...e}),c),n(()=>{var e;null===(e=s)||void 0===e||e.destroy(),s=null}),()=>i("div",{ref:p})}});export{a as ConnectButton};
1
+ import{defineComponent as e,ref as t,onMounted as n,watch as o,onBeforeUnmount as r,h as i}from"vue";import{mountConnectButton as l}from"@withone/connect";const d={type:Boolean,default:void 0},a=e({name:"OneConnectButton",props:{authorizeUrl:{type:String,required:!0},platforms:{type:Array,default:void 0},connected:d,disabled:d,variant:{type:String,default:void 0},size:{type:String,default:void 0},fullWidth:d,theme:{type:String,default:void 0},connectTheme:{type:String,default:void 0},appTheme:{type:String,default:void 0},accentColor:{type:String,default:void 0},label:{type:String,default:void 0},connectedLabel:{type:String,default:void 0},description:{type:String,default:void 0}},emits:{success:()=>!0,error:(e,t)=>"string"==typeof e&&"string"==typeof t,cancel:()=>!0},setup(e,{emit:d}){const a=t(null);let u=null;const c=()=>({...e,onSuccess:()=>d("success"),onError:(e,t)=>d("error",e,t),onCancel:()=>d("cancel")});return n(()=>{a.value&&(u=l(a.value,c()))}),o(()=>({...e}),()=>{var e;return null===(e=u)||void 0===e?void 0:e.update(c())},{deep:!0}),r(()=>{var e;null===(e=u)||void 0===e||e.destroy(),u=null}),()=>i("div",{ref:a})}});export{a as ConnectButton};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/connect",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",
@@ -27,8 +27,11 @@
27
27
  "@tsconfig/recommended": "^1.0.3",
28
28
  "@types/node": "^22.15.0",
29
29
  "@types/react": "^19.2.18",
30
+ "@types/react-dom": "^19.3.0",
31
+ "happy-dom": "^20.14.5",
30
32
  "prettier": "^3.2.5",
31
33
  "react": "^19.2.8",
34
+ "react-dom": "^19.3.0",
32
35
  "rollup": "^4.12.0",
33
36
  "rollup-plugin-typescript2": "^0.36.0",
34
37
  "tslib": "^2.6.2",
@@ -72,7 +72,9 @@ export const oneConnect = createOneConnect({
72
72
  tokenStore: {
73
73
  saveTokens: (userId, tokens) => /* write to this app's database, encrypted, keyed by its user */,
74
74
  loadTokens: (userId) => /* read; null when the user never connected */,
75
- clearTokens: (userId) => /* delete */,
75
+ clearTokens: (userId, failed) => /* delete; when `failed` is set, only if the stored refreshToken is still failed.refreshToken */,
76
+ // Required when the app runs more than one process (serverless, several instances, a worker):
77
+ withLock: (userId, run) => /* run() while holding a per-user lock all processes share, e.g. pg_advisory_xact_lock */,
76
78
  },
77
79
  });
78
80
  ```
@@ -81,6 +83,14 @@ export const oneConnect = createOneConnect({
81
83
  id as the key. Implement the store on whatever the app already uses; do not
82
84
  add a database for it.
83
85
 
86
+ One rotates the refresh token on every refresh and revokes the whole grant if
87
+ an old one is used again. So when two processes could refresh the same user,
88
+ `withLock` is not optional: without it, a web request and a worker refreshing
89
+ together disconnect the user. The README's "Tokens" section has a Postgres
90
+ version. For background work, call `refreshIfExpiring(userId, { withinMs })`:
91
+ every few minutes with a window of minutes to keep access tokens warm, and
92
+ once a day with a window of days so idle users' 30-day refresh tokens renew.
93
+
84
94
  ## 4 - The two routes
85
95
 
86
96
  Next.js App Router (also Remix, SvelteKit, Hono, Bun: anything with the web `Request`):
@@ -116,32 +126,50 @@ The routes mint `state` and PKCE, keep them in a per-flow httpOnly cookie
116
126
  (`one_tx_<state>`, SameSite=Lax, 30 minutes), verify the state on return,
117
127
  exchange the code with the secret over HTTP Basic, store both tokens, and
118
128
  redirect to `/` with `?one_connect=success` or
119
- `?one_connect=error&one_connect_message=...`. Pass `returnTo` to
129
+ `?one_connect=error&one_connect_error=declined|expired|failed` (a code, never
130
+ free text: the SDK shows fixed text for it). Pass `returnTo` to
120
131
  `createOneConnect` to land somewhere else.
121
132
 
122
133
  ## 5 - The button
123
134
 
124
135
  ```tsx
125
- import { ConnectButton } from "@withone/connect/react";
136
+ import { ConnectButton } from "@withone/connect/react"; // "use client" bundle: fine in a Server Component
126
137
 
127
138
  <ConnectButton
128
139
  authorizeUrl="/api/one/authorize"
129
- platforms={["stripe", "google-calendar"]} // connector slugs; logos and names come from One
130
- onSuccess={() => { /* refresh app state; the tokens are already stored */ }}
131
- onError={(message) => { /* show it; the user may simply have declined */ }}
140
+ platforms={["stripe", "google-calendar"]} // connector slugs; logos from One's CDN, names from the slug
141
+ connected={hasGrant} // from the server (e.g. await oneConnect.isConnected(userId))
142
+ onSuccess={() => { /* refetch app state; the tokens are already stored */ }}
143
+ onError={(message, code) => { /* show message; code: declined | expired | failed */ }}
132
144
  />
133
145
  ```
134
146
 
135
- Vue: `import { ConnectButton } from "@withone/connect/vue"` with `authorize-url`,
136
- `:platforms`, `@success`, `@error`. Svelte: `import { connectButton } from
137
- "@withone/connect/svelte"` as `use:connectButton={{ authorizeUrl, platforms,
138
- onSuccess }}`. Anything else: `import "@withone/connect"` registers
139
- `<one-connect-button authorize-url="/api/one/authorize" platforms="stripe, notion">`,
140
- which dispatches `success` and `error` events. A custom element:
141
- `useOneConnect({ authorizeUrl, onSuccess, onError }).open` on a click.
142
-
143
- The flow is a same-tab redirect. `onSuccess` and `onError` fire once when the
144
- tab comes back, and the SDK removes the `one_connect` params from the URL.
147
+ Other props: `disabled`, `variant` ("default" | "accent" | "block"), `size`
148
+ ("sm" | "md" | "lg"), `fullWidth`, `theme` ("light" | "dark" | "auto"),
149
+ `connectTheme` (One's page), `accentColor`, `label`, `connectedLabel`,
150
+ `description` (block), `onCancel` (user pressed Back on One's page).
151
+
152
+ Always pass `connected` from the server. Without it the button forgets after
153
+ a reload and asks the user to connect again.
154
+
155
+ Vue: `import { ConnectButton } from "@withone/connect/vue"` with the same props
156
+ in kebab case (`authorize-url`, `:connected`), and `@success`, `@error`,
157
+ `@cancel`. Svelte: `import { connectButton } from "@withone/connect/svelte"`
158
+ as `use:connectButton={{ authorizeUrl, platforms, connected, onSuccess }}`.
159
+ Anything else: `import "@withone/connect"` registers
160
+ `<one-connect-button authorize-url="/api/one/authorize" platforms="stripe, notion" connected>`,
161
+ which dispatches `success`, `error` (detail `{ message, code }`) and `cancel`.
162
+ A custom button in React: `const { open, status, error } = useOneConnect({ authorizeUrl })`
163
+ from `@withone/connect/react`. Elsewhere: `createConnectFlow({ authorizeUrl, onSuccess, onError }).open`.
164
+
165
+ The button renders in a shadow root with a constructed stylesheet, so it
166
+ works under a strict CSP. Allow `https://assets.withone.ai` in `img-src` for
167
+ the logos. Style it with `--one-connect-font`, `--one-connect-radius` and
168
+ `::part(button)`; do not wrap it in extra styling divs.
169
+
170
+ The flow is a same-tab redirect. The outcome is read once per page load:
171
+ every button shows it, and the callbacks fire once, on the first button
172
+ still mounted.
145
173
 
146
174
  ## 6 - Calling One with the grant
147
175
 
@@ -170,14 +198,22 @@ grant; the provider was never called. Do not retry. Any other `/v1` call:
170
198
  `oneConnect.fetch(userId, "/connections", init)`.
171
199
 
172
200
  `OneConnectError` codes: `not_connected` (no tokens stored), `refresh_failed`
173
- (One refused the refresh; the tokens were cleared; ask the user to connect
174
- again), `request_failed` (One answered with an error; `status` carries it).
201
+ (One declared the grant dead: revoked, expired or reused; the tokens were
202
+ cleared; ask the user to connect again), `request_failed` (One answered with
203
+ an error or could not be reached; `status` carries it; during a refresh the
204
+ tokens are KEPT, so retry later rather than asking the user to reconnect).
205
+
206
+ To ask users for more tools later, edit the app's permission set in the
207
+ dashboard. The next Connect shows only the new tools as "needs one more
208
+ connection" and the callback stores the new pair; no code change.
175
209
 
176
210
  ## 7 - Rules
177
211
 
178
212
  - Never put the secret in a client bundle, a log line or an error report.
179
213
  - Store tokens encrypted, keyed by the app's user. Delete them when the user
180
- is deleted. The client deletes them itself when a refresh fails.
214
+ is deleted. The client deletes them itself only when One declares the
215
+ grant dead, and never deletes a newer pair saved in the meantime.
216
+ - Give the store `withLock` whenever more than one process can refresh.
181
217
  - The registered redirect URI and `ONE_REDIRECT_URI` must be the same string.
182
218
  - Do not build a completion page. The callback redirect is the completion.
183
219
  - Do not write the OAuth steps by hand when the package exposes them.