@withone/connect 0.8.3 → 0.9.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.
Files changed (55) hide show
  1. package/README.md +158 -370
  2. package/dist/button.d.ts +9 -10
  3. package/dist/constants.d.ts +8 -4
  4. package/dist/index.cjs.js +1 -0
  5. package/dist/index.d.ts +3 -1
  6. package/dist/index.esm.js +1 -1
  7. package/dist/next.cjs.js +104 -0
  8. package/dist/next.d.ts +36 -0
  9. package/dist/next.esm.js +98 -0
  10. package/dist/node.cjs.js +69 -0
  11. package/dist/node.d.ts +33 -0
  12. package/dist/node.esm.js +67 -0
  13. package/dist/platforms.d.ts +21 -0
  14. package/dist/react.cjs.js +1 -1
  15. package/dist/react.d.ts +2 -19
  16. package/dist/react.esm.js +1 -1
  17. package/dist/return.d.ts +11 -0
  18. package/dist/server/index.cjs.js +349 -0
  19. package/dist/server/index.d.ts +32 -0
  20. package/dist/server/index.esm.js +344 -0
  21. package/dist/server/oauth.d.ts +24 -0
  22. package/dist/server/types.d.ts +136 -0
  23. package/dist/svelte.cjs.js +1 -1
  24. package/dist/svelte.d.ts +14 -4
  25. package/dist/svelte.esm.js +1 -1
  26. package/dist/types.d.ts +80 -0
  27. package/dist/useOneConnect.d.ts +13 -2
  28. package/dist/vue.cjs.js +1 -1
  29. package/dist/vue.d.ts +20 -18
  30. package/dist/vue.esm.js +1 -1
  31. package/dist/wrapper-options.d.ts +20 -15
  32. package/package.json +37 -15
  33. package/skills/one-connect/SKILL.md +194 -0
  34. package/src/button.ts +106 -157
  35. package/src/constants.ts +10 -4
  36. package/src/index.ts +17 -2
  37. package/src/next.ts +133 -0
  38. package/src/node.ts +107 -0
  39. package/src/platforms.ts +89 -0
  40. package/src/react.ts +23 -66
  41. package/src/return.ts +30 -0
  42. package/src/server/index.ts +382 -0
  43. package/src/server/oauth.ts +98 -0
  44. package/src/server/types.ts +155 -0
  45. package/src/svelte.ts +15 -15
  46. package/src/types.ts +87 -0
  47. package/src/useOneConnect.ts +36 -69
  48. package/src/vue.ts +23 -29
  49. package/src/wrapper-options.ts +46 -23
  50. package/dist/index.umd.js +0 -1
  51. package/dist/types/index.d.ts +0 -92
  52. package/src/react-types.d.ts +0 -23
  53. package/src/svelte-types.d.ts +0 -27
  54. package/src/types/index.d.ts +0 -92
  55. package/src/vue-types.d.ts +0 -16
@@ -1,2 +1,13 @@
1
- import type { OneConnectHandle, OneConnectProps } from "./types";
2
- export declare const useOneConnect: (props: OneConnectProps) => OneConnectHandle;
1
+ import type { OneConnectHandle, OneConnectOptions } from "./types";
2
+ /**
3
+ * Wires the connect flow to any element. A plain function rather than a
4
+ * React hook, so it works from every framework: call it once where the
5
+ * button lives and pass `open` to a click.
6
+ *
7
+ * The flow is a full-page redirect: the tab goes to the app's authorize
8
+ * route, on to One's hosted page, back to the app's callback route, and
9
+ * home. On the way home the callback route appends `?one_connect=…` to
10
+ * the URL it redirects to; this function reads that on load, fires
11
+ * onSuccess or onError once, and removes the params from the address bar.
12
+ */
13
+ export declare const useOneConnect: (options: OneConnectOptions) => OneConnectHandle;
package/dist/vue.cjs.js CHANGED
@@ -1 +1 @@
1
- "use strict";var e=require("vue");function t(e,t,n){return(t=function(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var o=n.call(e,t);if("object"!=typeof o)return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:t+""}(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}const n="one_connect",o="one_connect_message";let i=!1;const r=e=>{let t=!1;if("undefined"!=typeof window&&!i){const l=new URLSearchParams(window.location.search),c=l.get(n);if("success"===c||"error"===c){var r;i=!0;const a=null!==(r=l.get(o))&&void 0!==r?r:void 0,s=()=>{const e=new URLSearchParams(window.location.search);(e.has(n)||e.has(o))&&(e.delete(n),e.delete(o),window.history.replaceState(null,"",window.location.pathname+(e.toString()?`?${e.toString()}`:"")+window.location.hash))};s();for(const e of[50,500,2e3])window.setTimeout(s,e);window.setTimeout(()=>{if(!t){t=!0;try{var n,o;"success"===c?null===(n=e.onSuccess)||void 0===n||n.call(e):null===(o=e.onError)||void 0===o||o.call(e,null!=a?a:"The connection was not completed.")}catch{}}},0)}}return{open:()=>{"undefined"!=typeof window&&window.location.assign((()=>{try{const t=new URL(e.authorize.url,"undefined"==typeof window?void 0:window.location.origin);return e.appTheme&&(t.hash=`one_theme=${e.appTheme}`),t.toString()}catch{return e.authorize.url}})())},close:e=>{}}},l="one-connect-button-styles",c="cubic-bezier(.2,.9,.25,1)",a={btnBg:"#0A0C0B",btnFg:"#ffffff",surface:"#ffffff",fg:"#0A0C0B",muted:"#6B7280",line:"#D1D5DB",shadow:"0 1px 2px rgba(10,12,11,.08), 0 4px 14px rgba(10,12,11,.06)"},s={btnBg:"#F2F5F3",btnFg:"#0A0C0B",surface:"#101312",fg:"#F2F5F3",muted:"#8A938E",line:"#2A302D",shadow:"0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.35)"},d="#CCFF00",p="#3FE3A5",u="#0A0C0B";function h(e){const t=e.trim().toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"");return t?`https://assets.withone.ai/connectors/${t}.svg`:""}const m=(e,t)=>{var n,o;!function(){if("undefined"==typeof document||document.getElementById(l))return;const e=document.createElement("style");e.id=l,e.textContent=[`.owcb{font:500 14.5px/1.2 inherit;font-family:inherit;letter-spacing:-.01em;cursor:pointer;border:0;display:inline-flex;align-items:center;gap:12px;padding:12px 16px 12px 14px;border-radius:12px;transition:transform .2s ${c},box-shadow .2s ${c},opacity .2s ${c}}`,".owcb:hover{transform:translateY(-1px)}",".owcb:active{transform:translateY(0)}",`.owcb:focus-visible{outline:2px solid ${p};outline-offset:3px}`,".owcb[disabled]{cursor:default;opacity:.75;transform:none}",".owcb-stack{display:inline-flex;align-items:center;flex-shrink:0}",`.owcb-chip{width:22px;height:22px;border-radius:6px;display:grid;place-items:center;overflow:hidden;margin-left:-7px;transition:margin-left .28s ${c};position:relative}`,".owcb-chip:first-child{margin-left:0}",".owcb-chip img{width:14px;height:14px;display:block;object-fit:contain}",".owcb-chip.owcb-more{font-family:ui-monospace,monospace;font-size:9px;font-weight:500;letter-spacing:-.02em}",".owcb:hover .owcb-chip{margin-left:-2px}",".owcb:hover .owcb-chip:first-child{margin-left:0}",".owcb-label{white-space:nowrap}",`.owcb-arrow{opacity:.5;transition:transform .2s ${c},opacity .2s ${c}}`,".owcb:hover .owcb-arrow{transform:translateX(2px);opacity:.8}",".owcb-spinner{width:16px;height:16px;border-radius:50%;border:2px solid currentColor;border-top-color:transparent;opacity:.7;animation:owcb-spin .7s linear infinite}","@keyframes owcb-spin{to{transform:rotate(360deg)}}",".owcb.owcb-block{display:flex;flex-direction:column;align-items:stretch;width:100%;max-width:420px;text-align:left;padding:16px;border-radius:14px;gap:0}",".owcb-block-top{display:flex;align-items:center;justify-content:space-between;gap:12px}",".owcb-block-title{font-size:15px;font-weight:600;letter-spacing:-.01em}",".owcb-block-sub{font-size:13px;line-height:1.5;margin-top:6px;font-weight:400}",".owcb-block-foot{margin-top:14px;padding-top:12px;display:flex;align-items:center;gap:6px;font-size:11.5px;font-weight:400}","@media (prefers-reduced-motion:reduce){.owcb,.owcb-chip,.owcb-arrow{transition:none}.owcb-spinner{animation-duration:1.4s}}"].join("\n"),document.head.appendChild(e)}();const i="dark"===t.theme?s:a,m=null!==(n=t.variant)&&void 0!==n?n:"default",f=null!==(o=t.label)&&void 0!==o?o:"Connect your apps";let b="idle",v=!1;const w=r({...t.connect,onSuccess:()=>{var e,n;y("connected"),null===(e=(n=t.connect).onSuccess)||void 0===e||e.call(n)},onError:e=>{var n,o;y("idle"),null===(n=(o=t.connect).onError)||void 0===n||n.call(o,e)},onClose:()=>{var e,n;y("idle"),null===(e=(n=t.connect).onClose)||void 0===e||e.call(n)}}),g=document.createElement("button");g.type="button",g.className="block"===m?"owcb owcb-block":"owcb",g.addEventListener("click",()=>{"idle"!==b&&"connected"!==b||(y("connecting"),w.open())});const x=()=>{var e,n,o;const r="accent"===m,l="block"===m,c=l?i.surface:r?null!==(e=t.accentColor)&&void 0!==e?e:d:i.btnBg,a=l?i.fg:r?u:i.btnFg,s=l?i.surface:c,v=u;g.style.background="connected"!==b||l?c:p,g.style.color="connected"!==b||l?a:u,g.style.boxShadow=l?`inset 0 0 0 1px ${i.line}`:"connected"===b?"none":r?`0 1px 2px rgba(10,12,11,.1), 0 6px 20px ${null!==(n=t.accentColor)&&void 0!==n?n:d}38`:i.shadow,g.disabled="connecting"===b,g.innerHTML="";const w=function(e,t,n){var o,i;const r=null!==(o=e.platforms)&&void 0!==o?o:[];if(0===r.length&&!e.moreCount)return null;const l=document.createElement("span");l.className="owcb-stack",l.setAttribute("aria-hidden","true");for(const e of r.slice(0,3)){var c;const n=document.createElement("span");n.className="owcb-chip",n.style.background="#ffffff",n.style.boxShadow=`0 0 0 1.5px ${t}`;const o=()=>{n.textContent=e.name.charAt(0).toUpperCase(),n.style.font="600 10px ui-monospace,monospace",n.style.color=u},i=null!==(c=e.imageUrl)&&void 0!==c?c:h(e.name);if(i){const e=document.createElement("img");e.alt="",e.src=i,e.addEventListener("error",()=>{e.remove(),o()}),n.appendChild(e)}else o();l.appendChild(n)}const a=(null!==(i=e.moreCount)&&void 0!==i?i:0)+Math.max(0,r.length-3);if(a>0){const e=document.createElement("span");e.className="owcb-chip owcb-more",e.style.background="#ffffff",e.style.boxShadow=`0 0 0 1.5px ${t}`,e.style.color=n,e.textContent=`+${a}`,l.appendChild(e)}return l}(t,s,v);if(l){var x;const e=document.createElement("span");e.className="owcb-block-top";const n=document.createElement("span");if(n.className="owcb-block-title",n.textContent="connecting"===b?"Connecting…":"connected"===b?null!==(x=t.connectedLabel)&&void 0!==x?x:"Connected":f,e.appendChild(n),"connecting"===b){const t=document.createElement("span");t.className="owcb-spinner",e.appendChild(t)}else w&&e.appendChild(w);if(g.appendChild(e),t.description&&"idle"===b){const e=document.createElement("span");e.className="owcb-block-sub",e.style.color=i.muted,e.textContent=t.description,g.appendChild(e)}const o=document.createElement("span");return o.className="owcb-block-foot",o.style.color=i.muted,o.style.borderTop=`1px solid ${i.line}`,o.innerHTML=`<span style="font-size:11px;color:${i.muted}">Secured by</span><span style="display:inline-flex;color:${i.fg}"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.4" style="flex-shrink:0"><circle cx="12" cy="12" r="8.5"/></svg></span><span style="font-size:12px;font-weight:600;letter-spacing:-0.02em;color:${i.fg}">one</span>`,void g.appendChild(o)}if("connecting"===b){const e=document.createElement("span");e.className="owcb-spinner",g.appendChild(e)}else if("connected"===b){const e=document.createElement("span");e.innerHTML='<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px"><path d="M3 8.5l3.5 3.5L13 5"/></svg>',e.style.display="inline-flex",g.appendChild(e)}else w&&g.appendChild(w);const y=document.createElement("span");if(y.className="owcb-label",y.textContent="connecting"===b?"Connecting…":"connected"===b?null!==(o=t.connectedLabel)&&void 0!==o?o:"Connected":f,g.appendChild(y),"idle"===b){const e=document.createElement("span");e.innerHTML='<svg class="owcb-arrow" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3l5 5-5 5"/></svg>',e.style.display="inline-flex",g.appendChild(e)}},y=e=>{v||(b=e,x())};return x(),e.appendChild(g),{setState:y,destroy:()=>{v=!0,w.close({keepResult:!0}),g.remove()}}};!function(){if("undefined"!=typeof window&&"undefined"!=typeof customElements&&!customElements.get("one-connect-button")){class e extends HTMLElement{constructor(...e){super(...e),t(this,"onSuccess",null),t(this,"onError",null),t(this,"onClose",null),t(this,"handle",null)}connectedCallback(){this.mount()}disconnectedCallback(){var e;null===(e=this.handle)||void 0===e||e.destroy(),this.handle=null}attributeChangedCallback(){this.isConnected&&this.handle&&this.mount()}mount(){var e,t,n,o,i,r,l,c;null===(e=this.handle)||void 0===e||e.destroy(),this.handle=null;const a=this.getAttribute("authorize-url");if(!a)return;let s;const d=this.getAttribute("platforms");if(d)try{const e=JSON.parse(d);Array.isArray(e)&&(s=e)}catch{}const p=this.getAttribute("more-count"),u=p?parseInt(p,10):void 0;this.handle=m(this,{connect:{authorize:{url:a},appTheme:null!==(t=this.getAttribute("app-theme"))&&void 0!==t?t:void 0,onSuccess:()=>{var e;null===(e=this.onSuccess)||void 0===e||e.call(this),this.dispatchEvent(new CustomEvent("success"))},onError:e=>{var t;null===(t=this.onError)||void 0===t||t.call(this,e),this.dispatchEvent(new CustomEvent("error",{detail:e}))},onClose:()=>{var e;null===(e=this.onClose)||void 0===e||e.call(this),this.dispatchEvent(new CustomEvent("close"))}},label:null!==(n=this.getAttribute("label"))&&void 0!==n?n:void 0,variant:null!==(o=this.getAttribute("variant"))&&void 0!==o?o:void 0,theme:null!==(i=this.getAttribute("theme"))&&void 0!==i?i:void 0,platforms:s,moreCount:Number.isFinite(u)?u:void 0,description:null!==(r=this.getAttribute("description"))&&void 0!==r?r:void 0,accentColor:null!==(l=this.getAttribute("accent-color"))&&void 0!==l?l:void 0,connectedLabel:null!==(c=this.getAttribute("connected-label"))&&void 0!==c?c:void 0})}}t(e,"observedAttributes",["authorize-url","app-theme","label","variant","theme","platforms","more-count","description","accent-color","connected-label"]),customElements.define("one-connect-button",e)}}();const f=e.defineComponent({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},moreCount:{type:Number,default:void 0},description:{type:String,default:void 0},accentColor:{type:String,default:void 0},connectedLabel:{type:String,default:void 0}},emits:["success","error","close"],setup(t,{emit:n}){const o=e.ref(null);let i=null;const r=()=>{var e;null===(e=i)||void 0===e||e.destroy(),i=null,o.value&&(i=m(o.value,function(e,t){const n=null!=t?t:e;return{connect:{authorize:{url:e.authorizeUrl},appTheme:e.appTheme,onSuccess:()=>{var e;return null===(e=n.onSuccess)||void 0===e?void 0:e.call(n)},onError:e=>{var t;return null===(t=n.onError)||void 0===t?void 0:t.call(n,e)},onClose:()=>{var e;return null===(e=n.onClose)||void 0===e?void 0:e.call(n)}},label:e.label,variant:e.variant,theme:e.theme,platforms:e.platforms,moreCount:e.moreCount,description:e.description,accentColor:e.accentColor,connectedLabel:e.connectedLabel}}({...t},{onSuccess:()=>n("success"),onError:e=>n("error",e),onClose:()=>n("close")})))};return e.onMounted(r),e.watch(()=>{var e;return[t.authorizeUrl,t.appTheme,t.label,t.variant,t.theme,JSON.stringify(null!==(e=t.platforms)&&void 0!==e?e:[]),t.moreCount,t.description,t.accentColor,t.connectedLabel]},r),e.onBeforeUnmount(()=>{var e;null===(e=i)||void 0===e||e.destroy(),i=null}),()=>e.h("div",{ref:o})}});exports.ConnectButton=f;
1
+ "use strict";var e=require("vue"),t=require("@withone/connect");const o=e.defineComponent({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},moreCount:{type:Number,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(o,{emit:n}){const r=e.ref(null);let u=null;const i=()=>{var e;null===(e=u)||void 0===e||e.destroy(),u=null,r.value&&(u=t.mountConnectButton(r.value,t.optionsFromProps({...o},{onSuccess:()=>n("success"),onError:e=>n("error",e)})))};return e.onMounted(i),e.watch(()=>t.propsIdentity({...o}),i),e.onBeforeUnmount(()=>{var e;null===(e=u)||void 0===e||e.destroy(),u=null}),()=>e.h("div",{ref:r})}});exports.ConnectButton=o;
package/dist/vue.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import type { PropType } from "vue";
2
- import type { ConnectButtonPlatform } from "./types";
2
+ import { type ConnectButtonPlatformInput, type ConnectButtonVariant, 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
8
  appTheme: {
9
- type: PropType<"light" | "dark">;
9
+ type: PropType<OneConnectTheme>;
10
10
  default: undefined;
11
11
  };
12
12
  label: {
@@ -14,15 +14,15 @@ export declare const ConnectButton: import("vue").DefineComponent<import("vue").
14
14
  default: undefined;
15
15
  };
16
16
  variant: {
17
- type: PropType<"default" | "accent" | "block">;
17
+ type: PropType<ConnectButtonVariant>;
18
18
  default: undefined;
19
19
  };
20
20
  theme: {
21
- type: PropType<"light" | "dark">;
21
+ type: PropType<OneConnectTheme>;
22
22
  default: undefined;
23
23
  };
24
24
  platforms: {
25
- type: PropType<ConnectButtonPlatform[]>;
25
+ type: PropType<ConnectButtonPlatformInput[]>;
26
26
  default: undefined;
27
27
  };
28
28
  moreCount: {
@@ -43,13 +43,16 @@ export declare const ConnectButton: import("vue").DefineComponent<import("vue").
43
43
  };
44
44
  }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
45
45
  [key: string]: any;
46
- }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("success" | "error" | "close")[], "success" | "error" | "close", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
46
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
47
+ success: () => true;
48
+ error: (message: string) => boolean;
49
+ }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
47
50
  authorizeUrl: {
48
51
  type: StringConstructor;
49
52
  required: true;
50
53
  };
51
54
  appTheme: {
52
- type: PropType<"light" | "dark">;
55
+ type: PropType<OneConnectTheme>;
53
56
  default: undefined;
54
57
  };
55
58
  label: {
@@ -57,15 +60,15 @@ export declare const ConnectButton: import("vue").DefineComponent<import("vue").
57
60
  default: undefined;
58
61
  };
59
62
  variant: {
60
- type: PropType<"default" | "accent" | "block">;
63
+ type: PropType<ConnectButtonVariant>;
61
64
  default: undefined;
62
65
  };
63
66
  theme: {
64
- type: PropType<"light" | "dark">;
67
+ type: PropType<OneConnectTheme>;
65
68
  default: undefined;
66
69
  };
67
70
  platforms: {
68
- type: PropType<ConnectButtonPlatform[]>;
71
+ type: PropType<ConnectButtonPlatformInput[]>;
69
72
  default: undefined;
70
73
  };
71
74
  moreCount: {
@@ -85,17 +88,16 @@ export declare const ConnectButton: import("vue").DefineComponent<import("vue").
85
88
  default: undefined;
86
89
  };
87
90
  }>> & Readonly<{
88
- onSuccess?: ((...args: any[]) => any) | undefined;
89
- onError?: ((...args: any[]) => any) | undefined;
90
- onClose?: ((...args: any[]) => any) | undefined;
91
+ onSuccess?: (() => any) | undefined;
92
+ onError?: ((message: string) => any) | undefined;
91
93
  }>, {
92
- appTheme: "light" | "dark";
93
94
  label: string;
94
- variant: "default" | "accent" | "block";
95
- theme: "light" | "dark";
96
- platforms: ConnectButtonPlatform[];
97
- moreCount: number;
95
+ variant: ConnectButtonVariant;
96
+ theme: OneConnectTheme;
97
+ platforms: ConnectButtonPlatformInput[];
98
98
  description: string;
99
+ appTheme: OneConnectTheme;
100
+ moreCount: number;
99
101
  accentColor: string;
100
102
  connectedLabel: string;
101
103
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
package/dist/vue.esm.js CHANGED
@@ -1 +1 @@
1
- import{defineComponent as e,ref as t,onMounted as n,watch as o,onBeforeUnmount as i,h as r}from"vue";function l(e,t,n){return(t=function(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var o=n.call(e,t);if("object"!=typeof o)return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:t+""}(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}const c="one_connect",a="one_connect_message";let s=!1;const d=e=>{let t=!1;if("undefined"!=typeof window&&!s){const o=new URLSearchParams(window.location.search),i=o.get(c);if("success"===i||"error"===i){var n;s=!0;const r=null!==(n=o.get(a))&&void 0!==n?n:void 0,l=()=>{const e=new URLSearchParams(window.location.search);(e.has(c)||e.has(a))&&(e.delete(c),e.delete(a),window.history.replaceState(null,"",window.location.pathname+(e.toString()?`?${e.toString()}`:"")+window.location.hash))};l();for(const e of[50,500,2e3])window.setTimeout(l,e);window.setTimeout(()=>{if(!t){t=!0;try{var n,o;"success"===i?null===(n=e.onSuccess)||void 0===n||n.call(e):null===(o=e.onError)||void 0===o||o.call(e,null!=r?r:"The connection was not completed.")}catch{}}},0)}}return{open:()=>{"undefined"!=typeof window&&window.location.assign((()=>{try{const t=new URL(e.authorize.url,"undefined"==typeof window?void 0:window.location.origin);return e.appTheme&&(t.hash=`one_theme=${e.appTheme}`),t.toString()}catch{return e.authorize.url}})())},close:e=>{}}},p="one-connect-button-styles",u="cubic-bezier(.2,.9,.25,1)",h={btnBg:"#0A0C0B",btnFg:"#ffffff",surface:"#ffffff",fg:"#0A0C0B",muted:"#6B7280",line:"#D1D5DB",shadow:"0 1px 2px rgba(10,12,11,.08), 0 4px 14px rgba(10,12,11,.06)"},m={btnBg:"#F2F5F3",btnFg:"#0A0C0B",surface:"#101312",fg:"#F2F5F3",muted:"#8A938E",line:"#2A302D",shadow:"0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.35)"},f="#CCFF00",b="#3FE3A5",v="#0A0C0B";function w(e){const t=e.trim().toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"");return t?`https://assets.withone.ai/connectors/${t}.svg`:""}const g=(e,t)=>{var n,o;!function(){if("undefined"==typeof document||document.getElementById(p))return;const e=document.createElement("style");e.id=p,e.textContent=[`.owcb{font:500 14.5px/1.2 inherit;font-family:inherit;letter-spacing:-.01em;cursor:pointer;border:0;display:inline-flex;align-items:center;gap:12px;padding:12px 16px 12px 14px;border-radius:12px;transition:transform .2s ${u},box-shadow .2s ${u},opacity .2s ${u}}`,".owcb:hover{transform:translateY(-1px)}",".owcb:active{transform:translateY(0)}",`.owcb:focus-visible{outline:2px solid ${b};outline-offset:3px}`,".owcb[disabled]{cursor:default;opacity:.75;transform:none}",".owcb-stack{display:inline-flex;align-items:center;flex-shrink:0}",`.owcb-chip{width:22px;height:22px;border-radius:6px;display:grid;place-items:center;overflow:hidden;margin-left:-7px;transition:margin-left .28s ${u};position:relative}`,".owcb-chip:first-child{margin-left:0}",".owcb-chip img{width:14px;height:14px;display:block;object-fit:contain}",".owcb-chip.owcb-more{font-family:ui-monospace,monospace;font-size:9px;font-weight:500;letter-spacing:-.02em}",".owcb:hover .owcb-chip{margin-left:-2px}",".owcb:hover .owcb-chip:first-child{margin-left:0}",".owcb-label{white-space:nowrap}",`.owcb-arrow{opacity:.5;transition:transform .2s ${u},opacity .2s ${u}}`,".owcb:hover .owcb-arrow{transform:translateX(2px);opacity:.8}",".owcb-spinner{width:16px;height:16px;border-radius:50%;border:2px solid currentColor;border-top-color:transparent;opacity:.7;animation:owcb-spin .7s linear infinite}","@keyframes owcb-spin{to{transform:rotate(360deg)}}",".owcb.owcb-block{display:flex;flex-direction:column;align-items:stretch;width:100%;max-width:420px;text-align:left;padding:16px;border-radius:14px;gap:0}",".owcb-block-top{display:flex;align-items:center;justify-content:space-between;gap:12px}",".owcb-block-title{font-size:15px;font-weight:600;letter-spacing:-.01em}",".owcb-block-sub{font-size:13px;line-height:1.5;margin-top:6px;font-weight:400}",".owcb-block-foot{margin-top:14px;padding-top:12px;display:flex;align-items:center;gap:6px;font-size:11.5px;font-weight:400}","@media (prefers-reduced-motion:reduce){.owcb,.owcb-chip,.owcb-arrow{transition:none}.owcb-spinner{animation-duration:1.4s}}"].join("\n"),document.head.appendChild(e)}();const i="dark"===t.theme?m:h,r=null!==(n=t.variant)&&void 0!==n?n:"default",l=null!==(o=t.label)&&void 0!==o?o:"Connect your apps";let c="idle",a=!1;const s=d({...t.connect,onSuccess:()=>{var e,n;y("connected"),null===(e=(n=t.connect).onSuccess)||void 0===e||e.call(n)},onError:e=>{var n,o;y("idle"),null===(n=(o=t.connect).onError)||void 0===n||n.call(o,e)},onClose:()=>{var e,n;y("idle"),null===(e=(n=t.connect).onClose)||void 0===e||e.call(n)}}),g=document.createElement("button");g.type="button",g.className="block"===r?"owcb owcb-block":"owcb",g.addEventListener("click",()=>{"idle"!==c&&"connected"!==c||(y("connecting"),s.open())});const x=()=>{var e,n,o;const a="accent"===r,s="block"===r,d=s?i.surface:a?null!==(e=t.accentColor)&&void 0!==e?e:f:i.btnBg,p=s?i.fg:a?v:i.btnFg,u=s?i.surface:d,h=v;g.style.background="connected"!==c||s?d:b,g.style.color="connected"!==c||s?p:v,g.style.boxShadow=s?`inset 0 0 0 1px ${i.line}`:"connected"===c?"none":a?`0 1px 2px rgba(10,12,11,.1), 0 6px 20px ${null!==(n=t.accentColor)&&void 0!==n?n:f}38`:i.shadow,g.disabled="connecting"===c,g.innerHTML="";const m=function(e,t,n){var o,i;const r=null!==(o=e.platforms)&&void 0!==o?o:[];if(0===r.length&&!e.moreCount)return null;const l=document.createElement("span");l.className="owcb-stack",l.setAttribute("aria-hidden","true");for(const e of r.slice(0,3)){var c;const n=document.createElement("span");n.className="owcb-chip",n.style.background="#ffffff",n.style.boxShadow=`0 0 0 1.5px ${t}`;const o=()=>{n.textContent=e.name.charAt(0).toUpperCase(),n.style.font="600 10px ui-monospace,monospace",n.style.color=v},i=null!==(c=e.imageUrl)&&void 0!==c?c:w(e.name);if(i){const e=document.createElement("img");e.alt="",e.src=i,e.addEventListener("error",()=>{e.remove(),o()}),n.appendChild(e)}else o();l.appendChild(n)}const a=(null!==(i=e.moreCount)&&void 0!==i?i:0)+Math.max(0,r.length-3);if(a>0){const e=document.createElement("span");e.className="owcb-chip owcb-more",e.style.background="#ffffff",e.style.boxShadow=`0 0 0 1.5px ${t}`,e.style.color=n,e.textContent=`+${a}`,l.appendChild(e)}return l}(t,u,h);if(s){var x;const e=document.createElement("span");e.className="owcb-block-top";const n=document.createElement("span");if(n.className="owcb-block-title",n.textContent="connecting"===c?"Connecting…":"connected"===c?null!==(x=t.connectedLabel)&&void 0!==x?x:"Connected":l,e.appendChild(n),"connecting"===c){const t=document.createElement("span");t.className="owcb-spinner",e.appendChild(t)}else m&&e.appendChild(m);if(g.appendChild(e),t.description&&"idle"===c){const e=document.createElement("span");e.className="owcb-block-sub",e.style.color=i.muted,e.textContent=t.description,g.appendChild(e)}const o=document.createElement("span");return o.className="owcb-block-foot",o.style.color=i.muted,o.style.borderTop=`1px solid ${i.line}`,o.innerHTML=`<span style="font-size:11px;color:${i.muted}">Secured by</span><span style="display:inline-flex;color:${i.fg}"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.4" style="flex-shrink:0"><circle cx="12" cy="12" r="8.5"/></svg></span><span style="font-size:12px;font-weight:600;letter-spacing:-0.02em;color:${i.fg}">one</span>`,void g.appendChild(o)}if("connecting"===c){const e=document.createElement("span");e.className="owcb-spinner",g.appendChild(e)}else if("connected"===c){const e=document.createElement("span");e.innerHTML='<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px"><path d="M3 8.5l3.5 3.5L13 5"/></svg>',e.style.display="inline-flex",g.appendChild(e)}else m&&g.appendChild(m);const y=document.createElement("span");if(y.className="owcb-label",y.textContent="connecting"===c?"Connecting…":"connected"===c?null!==(o=t.connectedLabel)&&void 0!==o?o:"Connected":l,g.appendChild(y),"idle"===c){const e=document.createElement("span");e.innerHTML='<svg class="owcb-arrow" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3l5 5-5 5"/></svg>',e.style.display="inline-flex",g.appendChild(e)}},y=e=>{a||(c=e,x())};return x(),e.appendChild(g),{setState:y,destroy:()=>{a=!0,s.close({keepResult:!0}),g.remove()}}};!function(){if("undefined"!=typeof window&&"undefined"!=typeof customElements&&!customElements.get("one-connect-button")){class e extends HTMLElement{constructor(...e){super(...e),l(this,"onSuccess",null),l(this,"onError",null),l(this,"onClose",null),l(this,"handle",null)}connectedCallback(){this.mount()}disconnectedCallback(){var e;null===(e=this.handle)||void 0===e||e.destroy(),this.handle=null}attributeChangedCallback(){this.isConnected&&this.handle&&this.mount()}mount(){var e,t,n,o,i,r,l,c;null===(e=this.handle)||void 0===e||e.destroy(),this.handle=null;const a=this.getAttribute("authorize-url");if(!a)return;let s;const d=this.getAttribute("platforms");if(d)try{const e=JSON.parse(d);Array.isArray(e)&&(s=e)}catch{}const p=this.getAttribute("more-count"),u=p?parseInt(p,10):void 0;this.handle=g(this,{connect:{authorize:{url:a},appTheme:null!==(t=this.getAttribute("app-theme"))&&void 0!==t?t:void 0,onSuccess:()=>{var e;null===(e=this.onSuccess)||void 0===e||e.call(this),this.dispatchEvent(new CustomEvent("success"))},onError:e=>{var t;null===(t=this.onError)||void 0===t||t.call(this,e),this.dispatchEvent(new CustomEvent("error",{detail:e}))},onClose:()=>{var e;null===(e=this.onClose)||void 0===e||e.call(this),this.dispatchEvent(new CustomEvent("close"))}},label:null!==(n=this.getAttribute("label"))&&void 0!==n?n:void 0,variant:null!==(o=this.getAttribute("variant"))&&void 0!==o?o:void 0,theme:null!==(i=this.getAttribute("theme"))&&void 0!==i?i:void 0,platforms:s,moreCount:Number.isFinite(u)?u:void 0,description:null!==(r=this.getAttribute("description"))&&void 0!==r?r:void 0,accentColor:null!==(l=this.getAttribute("accent-color"))&&void 0!==l?l:void 0,connectedLabel:null!==(c=this.getAttribute("connected-label"))&&void 0!==c?c:void 0})}}l(e,"observedAttributes",["authorize-url","app-theme","label","variant","theme","platforms","more-count","description","accent-color","connected-label"]),customElements.define("one-connect-button",e)}}();const x=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},moreCount:{type:Number,default:void 0},description:{type:String,default:void 0},accentColor:{type:String,default:void 0},connectedLabel:{type:String,default:void 0}},emits:["success","error","close"],setup(e,{emit:l}){const c=t(null);let a=null;const s=()=>{var t;null===(t=a)||void 0===t||t.destroy(),a=null,c.value&&(a=g(c.value,function(e,t){const n=null!=t?t:e;return{connect:{authorize:{url:e.authorizeUrl},appTheme:e.appTheme,onSuccess:()=>{var e;return null===(e=n.onSuccess)||void 0===e?void 0:e.call(n)},onError:e=>{var t;return null===(t=n.onError)||void 0===t?void 0:t.call(n,e)},onClose:()=>{var e;return null===(e=n.onClose)||void 0===e?void 0:e.call(n)}},label:e.label,variant:e.variant,theme:e.theme,platforms:e.platforms,moreCount:e.moreCount,description:e.description,accentColor:e.accentColor,connectedLabel:e.connectedLabel}}({...e},{onSuccess:()=>l("success"),onError:e=>l("error",e),onClose:()=>l("close")})))};return n(s),o(()=>{var t;return[e.authorizeUrl,e.appTheme,e.label,e.variant,e.theme,JSON.stringify(null!==(t=e.platforms)&&void 0!==t?t:[]),e.moreCount,e.description,e.accentColor,e.connectedLabel]},s),i(()=>{var e;null===(e=a)||void 0===e||e.destroy(),a=null}),()=>r("div",{ref:c})}});export{x as ConnectButton};
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},moreCount:{type:Number,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 v=()=>{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(v),o(()=>l({...e}),v),n(()=>{var e;null===(e=s)||void 0===e||e.destroy(),s=null}),()=>i("div",{ref:p})}});export{a as ConnectButton};
@@ -1,24 +1,29 @@
1
- import type { ConnectButtonOptions, ConnectButtonPlatform } from "./types";
2
- /** The flat prop shape every framework wrapper exposes (React props,
3
- * Vue props, Svelte action options) — one place to translate it into
4
- * the core mountConnectButton options. */
5
- export interface ConnectButtonWrapperProps {
1
+ import type { ConnectButtonOptions, ConnectButtonPlatformInput, ConnectButtonVariant, OneConnectTheme } from "./types";
2
+ /** The flat prop shape every framework wrapper exposes: React props,
3
+ * Vue props, the Svelte action's options. One place turns it into the
4
+ * core mountConnectButton options. */
5
+ export interface ConnectButtonProps {
6
+ /** The app's own backend authorize route; relative is fine. */
6
7
  authorizeUrl: string;
7
- appTheme?: "light" | "dark";
8
+ /** Theme of One's hosted page. */
9
+ appTheme?: OneConnectTheme;
8
10
  onSuccess?: () => void;
9
- onError?: (error: string) => void;
10
- onClose?: () => void;
11
+ onError?: (message: string) => void;
11
12
  label?: string;
12
- variant?: "default" | "accent" | "block";
13
- theme?: "light" | "dark";
14
- platforms?: ConnectButtonPlatform[];
13
+ variant?: ConnectButtonVariant;
14
+ /** Matches the host page. */
15
+ theme?: OneConnectTheme;
16
+ /** Connector slugs, or objects to override name or logo. */
17
+ platforms?: ConnectButtonPlatformInput[];
15
18
  moreCount?: number;
16
19
  description?: string;
17
20
  accentColor?: string;
18
21
  connectedLabel?: string;
19
22
  }
20
- export declare function optionsFromWrapperProps(props: ConnectButtonWrapperProps, callbacks?: {
23
+ export interface ConnectButtonCallbacks {
21
24
  onSuccess?: () => void;
22
- onError?: (error: string) => void;
23
- onClose?: () => void;
24
- }): ConnectButtonOptions;
25
+ onError?: (message: string) => void;
26
+ }
27
+ export declare function optionsFromProps(props: ConnectButtonProps, callbacks?: ConnectButtonCallbacks): ConnectButtonOptions;
28
+ /** The inputs whose change should remount the button. */
29
+ export declare function propsIdentity(props: ConnectButtonProps): string;
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@withone/connect",
3
- "version": "0.8.3",
4
- "description": "Frontend bindings for One Connect, a drop-in OAuth flow that lets your users grant your application scoped, revocable access to their own connected tools. The user owns the connections; you hold a token scoped to exactly what they granted.",
3
+ "version": "0.9.0",
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",
7
7
  "src",
8
+ "skills",
8
9
  "README.md",
9
10
  "LICENSE"
10
11
  ],
11
- "main": "dist/index.umd.js",
12
+ "main": "dist/index.cjs.js",
12
13
  "module": "dist/index.esm.js",
13
14
  "types": "dist/index.d.ts",
14
15
  "devDependencies": {
@@ -24,17 +25,15 @@
24
25
  "@rollup/plugin-node-resolve": "^15.2.3",
25
26
  "@rollup/plugin-terser": "^0.4.4",
26
27
  "@tsconfig/recommended": "^1.0.3",
28
+ "@types/node": "^22.15.0",
27
29
  "@types/react": "^19.2.18",
28
- "@typescript-eslint/eslint-plugin": "^7.0.0",
29
- "@typescript-eslint/parser": "^7.0.0",
30
- "eslint": "^8.57.0",
31
30
  "prettier": "^3.2.5",
32
31
  "react": "^19.2.8",
33
32
  "rollup": "^4.12.0",
34
- "rollup-plugin-copy": "^3.5.0",
35
33
  "rollup-plugin-typescript2": "^0.36.0",
36
34
  "tslib": "^2.6.2",
37
35
  "typescript": "^5.3.3",
36
+ "vitest": "^3.2.4",
38
37
  "vue": "^3.5.42"
39
38
  },
40
39
  "author": "@withoneai",
@@ -42,11 +41,15 @@
42
41
  "access": "public"
43
42
  },
44
43
  "scripts": {
45
- "prepublishOnly": "npm run build",
46
- "build": "rollup -c && npm run verify",
47
- "verify": "node -e \"const fs=require('fs'),p=require('./package.json');const miss=[p.main,p.module,p.types].filter(f=>!fs.existsSync(f));if(miss.length){console.error('Build output missing: '+miss.join(', '));process.exit(1)}console.log('Build verified: '+[p.main,p.module,p.types].join(', '))\"",
48
- "lint": "eslint src --ext .ts,.tsx",
49
- "format": "prettier --write \"src/**/*.{ts,tsx}\""
44
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
45
+ "build": "npm run clean && rollup -c && npm run declarations && npm run verify",
46
+ "verify": "node scripts/verify-build.js",
47
+ "typecheck": "tsc --noEmit -p tsconfig.tests.json",
48
+ "test": "vitest run",
49
+ "check": "npm run typecheck && npm run test && npm run build",
50
+ "prepublishOnly": "npm run check",
51
+ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
52
+ "declarations": "tsc -p tsconfig.build.json"
50
53
  },
51
54
  "repository": {
52
55
  "type": "git",
@@ -67,7 +70,7 @@
67
70
  ".": {
68
71
  "types": "./dist/index.d.ts",
69
72
  "import": "./dist/index.esm.js",
70
- "require": "./dist/index.umd.js"
73
+ "require": "./dist/index.cjs.js"
71
74
  },
72
75
  "./react": {
73
76
  "types": "./dist/react.d.ts",
@@ -83,7 +86,23 @@
83
86
  "types": "./dist/svelte.d.ts",
84
87
  "import": "./dist/svelte.esm.js",
85
88
  "require": "./dist/svelte.cjs.js"
86
- }
89
+ },
90
+ "./server": {
91
+ "types": "./dist/server/index.d.ts",
92
+ "import": "./dist/server/index.esm.js",
93
+ "require": "./dist/server/index.cjs.js"
94
+ },
95
+ "./next": {
96
+ "types": "./dist/next.d.ts",
97
+ "import": "./dist/next.esm.js",
98
+ "require": "./dist/next.cjs.js"
99
+ },
100
+ "./node": {
101
+ "types": "./dist/node.d.ts",
102
+ "import": "./dist/node.esm.js",
103
+ "require": "./dist/node.cjs.js"
104
+ },
105
+ "./package.json": "./package.json"
87
106
  },
88
107
  "peerDependencies": {
89
108
  "react": ">=17",
@@ -96,5 +115,8 @@
96
115
  "vue": {
97
116
  "optional": true
98
117
  }
118
+ },
119
+ "engines": {
120
+ "node": ">=18"
99
121
  }
100
- }
122
+ }
@@ -0,0 +1,194 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # One Connect
7
+
8
+ You are adding One Connect to this application. Its users will grant the app
9
+ scoped, revocable access to their own One-connected tools. The package does
10
+ the OAuth work; you wire three things: a button, the two routes, and the
11
+ calls you make with the grant.
12
+
13
+ ```
14
+ Browser Your backend One
15
+ <ConnectButton> ------> GET /api/one/authorize ---302---> One's hosted page (connect.withone.ai)
16
+ sign-in code, pick tools, set access
17
+ GET /api/one/callback <--302---- ?code=...&state=...
18
+ exchanges the code, stores the tokens
19
+ 302 -> /?one_connect=success
20
+ <------ SDK reads the param and fires onSuccess
21
+ Later: oneConnect.runAction(userId, ...) -> /v1/passthrough, grant enforced by One
22
+ ```
23
+
24
+ Secrets and tokens never reach the browser.
25
+
26
+ ## 1 - Collect from the human first
27
+
28
+ The human creates the app in the One dashboard: Developers -> Connect ->
29
+ New app. Confidential client. The secret is shown once.
30
+
31
+ | Value | Notes |
32
+ |---|---|
33
+ | `ONE_CLIENT_ID` | 40 hex characters |
34
+ | `ONE_CLIENT_SECRET` | starts with `one_secret_` |
35
+ | Registered redirect URI | Must be the exact URL of the callback route (scheme, host, path, no trailing slash). One compares the string as-is and answers `invalid redirect_uri` on any difference. Use `https` for anything public; `http://localhost:3000/api/one/callback` is fine locally. |
36
+ | `ONE_PERMISSION_SET` (optional) | The id of the app's ask: the connectors and levels the consent page opens on. Users can narrow it, never widen it. Without one, the page lists everything the user has connected. |
37
+ | Environment | Production is the default. For the development dashboard set `ONE_API_URL=https://development-api.withone.ai`. |
38
+
39
+ Two facts to tell the human:
40
+
41
+ - The app can show its users one line saying why it asks. It is set on the
42
+ app in the dashboard, not sent by this package.
43
+ - Users can change or revoke the grant from their One dashboard at any time.
44
+ Treat a `401` from One as "ask the user to reconnect", never as a bug.
45
+
46
+ ## 2 - Environment (server only)
47
+
48
+ ```bash
49
+ ONE_CLIENT_ID=...
50
+ ONE_CLIENT_SECRET=one_secret_...
51
+ ONE_REDIRECT_URI=https://yourapp.com/api/one/callback # exactly the registered value
52
+ ONE_PERMISSION_SET=... # optional
53
+ ONE_API_URL=https://api.withone.ai # optional; development-api.withone.ai for development
54
+ ```
55
+
56
+ ## 3 - Install and create the client
57
+
58
+ ```bash
59
+ npm install @withone/connect
60
+ ```
61
+
62
+ ```ts
63
+ // lib/one.ts (server only)
64
+ import { createOneConnect } from "@withone/connect/server";
65
+
66
+ export const oneConnect = createOneConnect({
67
+ clientId: process.env.ONE_CLIENT_ID!,
68
+ clientSecret: process.env.ONE_CLIENT_SECRET!,
69
+ redirectUri: process.env.ONE_REDIRECT_URI!,
70
+ permissionSet: process.env.ONE_PERMISSION_SET,
71
+ oneApiUrl: process.env.ONE_API_URL,
72
+ tokenStore: {
73
+ saveTokens: (userId, tokens) => /* write to this app's database, encrypted, keyed by its user */,
74
+ loadTokens: (userId) => /* read; null when the user never connected */,
75
+ clearTokens: (userId) => /* delete */,
76
+ },
77
+ });
78
+ ```
79
+
80
+ `tokens` is `{ accessToken, refreshToken, expiresAt }`. Use the app's own user
81
+ id as the key. Implement the store on whatever the app already uses; do not
82
+ add a database for it.
83
+
84
+ ## 4 - The two routes
85
+
86
+ Next.js App Router (also Remix, SvelteKit, Hono, Bun: anything with the web `Request`):
87
+
88
+ ```ts
89
+ // app/api/one/[action]/route.ts
90
+ import { createOneConnectRoutes } from "@withone/connect/next";
91
+ import { oneConnect } from "@/lib/one";
92
+
93
+ export const { GET } = createOneConnectRoutes(oneConnect, {
94
+ identifyUser: async (request) => /* this app's signed-in user id, or null */,
95
+ loginHintFor: async (request) => /* their email, to pre-fill One's sign-in; or null */,
96
+ signInUrl: "/login", // where to send a visitor who is not signed in
97
+ });
98
+ ```
99
+
100
+ That file serves `/api/one/authorize` and `/api/one/callback`.
101
+
102
+ Express, Fastify, Koa, plain Node:
103
+
104
+ ```ts
105
+ import { createOneConnectHandlers } from "@withone/connect/node";
106
+ import { oneConnect } from "./one";
107
+
108
+ const { authorize, callback } = createOneConnectHandlers(oneConnect, {
109
+ identifyUser: (request) => /* user id or null */,
110
+ });
111
+ app.get("/api/one/authorize", authorize);
112
+ app.get("/api/one/callback", callback);
113
+ ```
114
+
115
+ The routes mint `state` and PKCE, keep them in a per-flow httpOnly cookie
116
+ (`one_tx_<state>`, SameSite=Lax, 30 minutes), verify the state on return,
117
+ exchange the code with the secret over HTTP Basic, store both tokens, and
118
+ redirect to `/` with `?one_connect=success` or
119
+ `?one_connect=error&one_connect_message=...`. Pass `returnTo` to
120
+ `createOneConnect` to land somewhere else.
121
+
122
+ ## 5 - The button
123
+
124
+ ```tsx
125
+ import { ConnectButton } from "@withone/connect/react";
126
+
127
+ <ConnectButton
128
+ 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 */ }}
132
+ />
133
+ ```
134
+
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.
145
+
146
+ ## 6 - Calling One with the grant
147
+
148
+ Everything runs on the server through the client. Tokens refresh themselves.
149
+
150
+ ```ts
151
+ const connections = await oneConnect.listConnections(userId);
152
+ // [{ key, platform, name, title, image, access }]
153
+ // access.policy: "full" | "methods" (+ methods: ["GET", ...]) | "actions" (+ actions: [{ actionId, title, method }])
154
+
155
+ const actions = await oneConnect.listActions(userId, "gmail");
156
+ // [{ _id, title, method, path }] - what exists, not what is permitted
157
+
158
+ const reply = await oneConnect.runAction(userId, {
159
+ connectionKey: connection.key,
160
+ actionId: action._id,
161
+ method: action.method,
162
+ path: action.path,
163
+ body: payload,
164
+ });
165
+ // { status, ok, blockedByGrant, data }
166
+ ```
167
+
168
+ `blockedByGrant` true means One refused the call because it is outside the
169
+ grant; the provider was never called. Do not retry. Any other `/v1` call:
170
+ `oneConnect.fetch(userId, "/connections", init)`.
171
+
172
+ `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).
175
+
176
+ ## 7 - Rules
177
+
178
+ - Never put the secret in a client bundle, a log line or an error report.
179
+ - 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.
181
+ - The registered redirect URI and `ONE_REDIRECT_URI` must be the same string.
182
+ - Do not build a completion page. The callback redirect is the completion.
183
+ - Do not write the OAuth steps by hand when the package exposes them.
184
+
185
+ ## 8 - Done when all of these pass
186
+
187
+ 1. Button -> One's hosted page -> sign-in code from a real inbox -> choose
188
+ tools and levels -> "You're all set" -> back in the app with `onSuccess`
189
+ fired. Repeat once in a private window.
190
+ 2. `listConnections` returns only the granted connections, each with `access`.
191
+ 3. `runAction` on an action inside the grant reaches the provider; one outside
192
+ it returns `blockedByGrant: true`.
193
+ 4. Revoking the app from the user's One dashboard makes the next call throw
194
+ `refresh_failed` or return `401`, and the app shows its reconnect prompt.