@withone/connect 0.8.2 → 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 -137
  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
package/dist/react.d.ts CHANGED
@@ -1,23 +1,6 @@
1
1
  import type { CSSProperties, ReactElement } from "react";
2
- import type { ConnectButtonPlatform } from "./types";
3
- export interface ConnectButtonProps {
4
- /** The app's own backend authorize route ("/api/one/authorize" is
5
- * fine — relative resolves against the page). */
6
- authorizeUrl: string;
7
- /** Theme for One's hosted page (rides the URL fragment). */
8
- appTheme?: "light" | "dark";
9
- onSuccess?: () => void;
10
- onError?: (error: string) => void;
11
- onClose?: () => void;
12
- label?: string;
13
- variant?: "default" | "accent" | "block";
14
- /** Matches the host page, not One's card. */
15
- theme?: "light" | "dark";
16
- platforms?: ConnectButtonPlatform[];
17
- moreCount?: number;
18
- description?: string;
19
- accentColor?: string;
20
- connectedLabel?: string;
2
+ import { type ConnectButtonProps as ConnectButtonCoreProps } from "@withone/connect";
3
+ export interface ConnectButtonProps extends ConnectButtonCoreProps {
21
4
  className?: string;
22
5
  style?: CSSProperties;
23
6
  }
package/dist/react.esm.js CHANGED
@@ -1 +1 @@
1
- import{useRef as e,useEffect as t,createElement as n}from"react";function o(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 i="one_connect",r="one_connect_message";let c=!1;const l=e=>{let t=!1;if("undefined"!=typeof window&&!c){const o=new URLSearchParams(window.location.search),l=o.get(i);if("success"===l||"error"===l){var n;c=!0;const s=null!==(n=o.get(r))&&void 0!==n?n:void 0,a=()=>{const e=new URLSearchParams(window.location.search);(e.has(i)||e.has(r))&&(e.delete(i),e.delete(r),window.history.replaceState(null,"",window.location.pathname+(e.toString()?`?${e.toString()}`:"")+window.location.hash))};a();for(const e of[50,500,2e3])window.setTimeout(a,e);window.setTimeout(()=>{if(!t){t=!0;try{var n,o;"success"===l?null===(n=e.onSuccess)||void 0===n||n.call(e):null===(o=e.onError)||void 0===o||o.call(e,null!=s?s:"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=>{}}},s="one-connect-button-styles",a="cubic-bezier(.2,.9,.25,1)",d={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)"},p={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)"},u="#CCFF00",h="#3FE3A5",m="#0A0C0B";const f=(e,t)=>{var n,o;!function(){if("undefined"==typeof document||document.getElementById(s))return;const e=document.createElement("style");e.id=s,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 ${a},box-shadow .2s ${a},opacity .2s ${a}}`,".owcb:hover{transform:translateY(-1px)}",".owcb:active{transform:translateY(0)}",`.owcb:focus-visible{outline:2px solid ${h};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 ${a};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 ${a},opacity .2s ${a}}`,".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?p:d,r=null!==(n=t.variant)&&void 0!==n?n:"default",c=null!==(o=t.label)&&void 0!==o?o:"Connect your apps";let f="idle",b=!1;const w=l({...t.connect,onSuccess:()=>{var e,n;x("connected"),null===(e=(n=t.connect).onSuccess)||void 0===e||e.call(n)},onError:e=>{var n,o;x("idle"),null===(n=(o=t.connect).onError)||void 0===n||n.call(o,e)},onClose:()=>{var e,n;x("idle"),null===(e=(n=t.connect).onClose)||void 0===e||e.call(n)}}),v=document.createElement("button");v.type="button",v.className="block"===r?"owcb owcb-block":"owcb",v.addEventListener("click",()=>{"idle"!==f&&"connected"!==f||(x("connecting"),w.open())});const g=()=>{var e,n,o;const l="accent"===r,s="block"===r,a=s?i.surface:l?null!==(e=t.accentColor)&&void 0!==e?e:u:i.btnBg,d=s?i.fg:l?m:i.btnFg,p=s?i.surface:a,b=m;v.style.background="connected"!==f||s?a:h,v.style.color="connected"!==f||s?d:m,v.style.boxShadow=s?`inset 0 0 0 1px ${i.line}`:"connected"===f?"none":l?`0 1px 2px rgba(10,12,11,.1), 0 6px 20px ${null!==(n=t.accentColor)&&void 0!==n?n:u}38`:i.shadow,v.disabled="connecting"===f,v.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 c=document.createElement("span");c.className="owcb-stack",c.setAttribute("aria-hidden","true");for(const e of r.slice(0,3)){const n=document.createElement("span");if(n.className="owcb-chip",n.style.background="#ffffff",n.style.boxShadow=`0 0 0 1.5px ${t}`,e.imageUrl){const t=document.createElement("img");t.alt="",t.src=e.imageUrl,t.addEventListener("error",()=>{t.style.display="none",n.textContent=e.name.charAt(0).toUpperCase(),n.style.font="600 10px ui-monospace,monospace",n.style.color=m}),n.appendChild(t)}else n.textContent=e.name.charAt(0).toUpperCase(),n.style.font="600 10px ui-monospace,monospace",n.style.color=m;c.appendChild(n)}const l=(null!==(i=e.moreCount)&&void 0!==i?i:0)+Math.max(0,r.length-3);if(l>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=`+${l}`,c.appendChild(e)}return c}(t,p,b);if(s){var g;const e=document.createElement("span");e.className="owcb-block-top";const n=document.createElement("span");if(n.className="owcb-block-title",n.textContent="connecting"===f?"Connecting…":"connected"===f?null!==(g=t.connectedLabel)&&void 0!==g?g:"Connected":c,e.appendChild(n),"connecting"===f){const t=document.createElement("span");t.className="owcb-spinner",e.appendChild(t)}else w&&e.appendChild(w);if(v.appendChild(e),t.description&&"idle"===f){const e=document.createElement("span");e.className="owcb-block-sub",e.style.color=i.muted,e.textContent=t.description,v.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 v.appendChild(o)}if("connecting"===f){const e=document.createElement("span");e.className="owcb-spinner",v.appendChild(e)}else if("connected"===f){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",v.appendChild(e)}else w&&v.appendChild(w);const x=document.createElement("span");if(x.className="owcb-label",x.textContent="connecting"===f?"Connecting…":"connected"===f?null!==(o=t.connectedLabel)&&void 0!==o?o:"Connected":c,v.appendChild(x),"idle"===f){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",v.appendChild(e)}},x=e=>{b||(f=e,g())};return g(),e.appendChild(v),{setState:x,destroy:()=>{b=!0,w.close({keepResult:!0}),v.remove()}}};function b(o){var i;const r=e(null),c=e(null),l=e({onSuccess:o.onSuccess,onError:o.onError,onClose:o.onClose});l.current={onSuccess:o.onSuccess,onError:o.onError,onClose:o.onClose};const s=JSON.stringify(null!==(i=o.platforms)&&void 0!==i?i:[]);return t(()=>{if(!r.current)return;const e={connect:{authorize:{url:o.authorizeUrl},appTheme:o.appTheme,onSuccess:()=>{var e,t;return null===(e=(t=l.current).onSuccess)||void 0===e?void 0:e.call(t)},onError:e=>{var t,n;return null===(t=(n=l.current).onError)||void 0===t?void 0:t.call(n,e)},onClose:()=>{var e,t;return null===(e=(t=l.current).onClose)||void 0===e?void 0:e.call(t)}},label:o.label,variant:o.variant,theme:o.theme,platforms:o.platforms,moreCount:o.moreCount,description:o.description,accentColor:o.accentColor,connectedLabel:o.connectedLabel};return c.current=f(r.current,e),()=>{var e;null===(e=c.current)||void 0===e||e.destroy(),c.current=null}},[o.authorizeUrl,o.appTheme,o.label,o.variant,o.theme,s,o.moreCount,o.description,o.accentColor,o.connectedLabel]),n("div",{ref:r,className:o.className,style:o.style})}!function(){if("undefined"!=typeof window&&"undefined"!=typeof customElements&&!customElements.get("one-connect-button")){class e extends HTMLElement{constructor(...e){super(...e),o(this,"onSuccess",null),o(this,"onError",null),o(this,"onClose",null),o(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,c,l;null===(e=this.handle)||void 0===e||e.destroy(),this.handle=null;const s=this.getAttribute("authorize-url");if(!s)return;let a;const d=this.getAttribute("platforms");if(d)try{const e=JSON.parse(d);Array.isArray(e)&&(a=e)}catch{}const p=this.getAttribute("more-count"),u=p?parseInt(p,10):void 0;this.handle=f(this,{connect:{authorize:{url:s},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:a,moreCount:Number.isFinite(u)?u:void 0,description:null!==(r=this.getAttribute("description"))&&void 0!==r?r:void 0,accentColor:null!==(c=this.getAttribute("accent-color"))&&void 0!==c?c:void 0,connectedLabel:null!==(l=this.getAttribute("connected-label"))&&void 0!==l?l:void 0})}}o(e,"observedAttributes",["authorize-url","app-theme","label","variant","theme","platforms","more-count","description","accent-color","connected-label"]),customElements.define("one-connect-button",e)}}();export{b as ConnectButton};
1
+ import{useRef as r,useEffect as n,createElement as o}from"react";import{propsIdentity as c,mountConnectButton as e,optionsFromProps as u}from"@withone/connect";function t(t){const s=r(null),l=r(null),i=r({onSuccess:t.onSuccess,onError:t.onError});i.current={onSuccess:t.onSuccess,onError:t.onError};const a=c(t);return n(()=>{if(s.current)return l.current=e(s.current,u(t,{onSuccess:()=>{var r,n;return null===(r=(n=i.current).onSuccess)||void 0===r?void 0:r.call(n)},onError:r=>{var n,o;return null===(n=(o=i.current).onError)||void 0===n?void 0:n.call(o,r)}})),()=>{var r;null===(r=l.current)||void 0===r||r.destroy(),l.current=null}},[a]),o("div",{ref:s,className:t.className,style:t.style})}export{t as ConnectButton};
@@ -0,0 +1,11 @@
1
+ import type { OneConnectReturn } from "./types";
2
+ /** Reads the outcome the callback route put on the return URL. */
3
+ export declare function parseReturn(search: string): OneConnectReturn | null;
4
+ /** The same URL without the return params, so a refresh does not
5
+ * re-fire the callbacks. */
6
+ export declare function stripReturnParams(location: {
7
+ pathname: string;
8
+ search: string;
9
+ hash: string;
10
+ }): string;
11
+ export declare function hasReturnParams(search: string): boolean;
@@ -0,0 +1,349 @@
1
+ 'use strict';
2
+
3
+ var node_crypto = require('node:crypto');
4
+
5
+ /** Query params the app's callback route puts on its final redirect.
6
+ * The SDK reads them off the page URL when the tab comes home, so
7
+ * there is no completion page to build. */
8
+ const RETURN_STATUS_PARAM = "one_connect";
9
+ const RETURN_MESSAGE_PARAM = "one_connect_message";
10
+
11
+ /** One's production API. Point `oneApiUrl` elsewhere for development. */
12
+ const DEFAULT_ONE_API_URL = "https://api.withone.ai";
13
+
14
+ /** One cookie per flow, named by its state, so a retry or a second tab
15
+ * never overwrites the verifier of a flow still in progress. */
16
+ const TX_COOKIE_PREFIX = "one_tx_";
17
+
18
+ /** One gives the consent 10 minutes and then the code 10 minutes, so a
19
+ * legitimate callback can arrive up to twenty minutes after the start.
20
+ * Thirty minutes leaves a margin. */
21
+ const TX_COOKIE_MAX_AGE_SECONDS = 1800;
22
+ const DEFAULT_SCOPES = ["user:connections:read", "user:connections:write", "org:connections:read", "org:connections:write", "project:connections:read", "project:connections:write"];
23
+ function createState() {
24
+ return node_crypto.randomBytes(16).toString("hex");
25
+ }
26
+ function createPkceVerifier() {
27
+ return node_crypto.randomBytes(32).toString("base64url");
28
+ }
29
+ function pkceChallenge(verifier) {
30
+ return node_crypto.createHash("sha256").update(verifier).digest("base64url");
31
+ }
32
+ function txCookieName(state) {
33
+ return `${TX_COOKIE_PREFIX}${state}`;
34
+ }
35
+
36
+ /** The cookie scoped to the routes that need it: the directory of the
37
+ * callback path, which is also where the authorize route lives. */
38
+ function txCookie(state, verifier, redirectUri) {
39
+ const url = new URL(redirectUri);
40
+ const path = url.pathname.replace(/\/[^/]*$/, "") || "/";
41
+ return {
42
+ name: txCookieName(state),
43
+ value: verifier,
44
+ options: {
45
+ httpOnly: true,
46
+ secure: url.protocol === "https:",
47
+ sameSite: "lax",
48
+ maxAge: TX_COOKIE_MAX_AGE_SECONDS,
49
+ path
50
+ }
51
+ };
52
+ }
53
+ function basicAuthorization(clientId, clientSecret) {
54
+ return `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
55
+ }
56
+
57
+ /** The tenant headers a grant needs on every call. A grant made into an
58
+ * organization or project lives there; One resolves the tenant from
59
+ * these headers, and without them the call runs in the user's personal
60
+ * space. The access token's claims name the tenant, so echo them. */
61
+ function tenancyHeaders(accessToken) {
62
+ try {
63
+ const payload = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString());
64
+ const headers = {};
65
+ const organizationId = payload.organization_ids?.[0];
66
+ const projectId = payload.project_ids?.[0];
67
+ if (organizationId) headers["X-One-Organization-Id"] = organizationId;
68
+ if (projectId) headers["X-One-Project-Id"] = projectId;
69
+ return headers;
70
+ } catch {
71
+ return {};
72
+ }
73
+ }
74
+
75
+ /** The scopes the token was granted, from its claims. Display only. */
76
+ function tokenScopes(accessToken) {
77
+ try {
78
+ const payload = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString());
79
+ return (payload.scope ?? "").split(" ").filter(Boolean);
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Types for `@withone/connect/server`: the half of the SDK that runs on
87
+ * the app's server and holds the client secret.
88
+ */
89
+
90
+ /** What the app stores per user after the exchange. */
91
+
92
+ /**
93
+ * Where the app keeps each user's tokens: its database, a cache, an
94
+ * encrypted cookie. The SDK never sees a token outside these three
95
+ * calls. `userId` is the app's own id for its user.
96
+ */
97
+
98
+ /** The transaction cookie the authorize leg sets and the callback reads. */
99
+
100
+ /** One connection the grant reaches, with the access it confers. */
101
+
102
+ /** One catalog action for a platform. */
103
+
104
+ class OneConnectError extends Error {
105
+ constructor(code, message, status) {
106
+ super(message);
107
+ this.name = "OneConnectError";
108
+ this.code = code;
109
+ this.status = status;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * `@withone/connect/server`: the half of One Connect that runs on the
115
+ * app's server. It starts the flow, completes it, keeps the tokens fresh
116
+ * and makes every call with the grant. The client secret never leaves
117
+ * here.
118
+ *
119
+ * const oneConnect = createOneConnect({ clientId, clientSecret,
120
+ * redirectUri, permissionSet, tokenStore });
121
+ *
122
+ * // in the authorize route
123
+ * const { redirectUrl, cookie } = oneConnect.startAuthorization({ loginHint });
124
+ * // in the callback route
125
+ * const result = await oneConnect.completeAuthorization({ userId, url, getCookie });
126
+ * // afterwards
127
+ * const rows = await oneConnect.listConnections(userId);
128
+ * const reply = await oneConnect.runAction(userId, { connectionKey, actionId, method, path });
129
+ *
130
+ * The Next.js and Node adapters turn the first two into route handlers.
131
+ */
132
+
133
+ /** Refresh this long before expiry, so a call never races the clock. */
134
+ const REFRESH_MARGIN_MS = 60_000;
135
+ const CATALOG_PAGE_SIZE = 100;
136
+ const CATALOG_MAX_PAGES = 20;
137
+ function createOneConnect(config) {
138
+ const oneApiUrl = (config.oneApiUrl ?? DEFAULT_ONE_API_URL).replace(/\/+$/, "");
139
+ const authorizeUrl = `${oneApiUrl}/oauth/authorize`;
140
+ const tokenUrl = `${oneApiUrl}/oauth/token`;
141
+ const apiUrl = `${oneApiUrl}/v1`;
142
+ const returnTo = config.returnTo ?? "/";
143
+ const scopes = config.scopes ?? DEFAULT_SCOPES;
144
+ const {
145
+ tokenStore
146
+ } = config;
147
+
148
+ /** One refresh in flight per user: two concurrent refreshes with the
149
+ * same refresh token trip One's reuse detection. */
150
+ const refreshing = new Map();
151
+ const returnUrl = (status, message) => {
152
+ const url = new URL(returnTo, config.redirectUri);
153
+ url.searchParams.set(RETURN_STATUS_PARAM, status);
154
+ if (message) url.searchParams.set(RETURN_MESSAGE_PARAM, message);
155
+ return url.toString();
156
+ };
157
+ const exchange = async body => {
158
+ const response = await fetch(tokenUrl, {
159
+ method: "POST",
160
+ headers: {
161
+ Authorization: basicAuthorization(config.clientId, config.clientSecret),
162
+ "Content-Type": "application/x-www-form-urlencoded"
163
+ },
164
+ body
165
+ });
166
+ if (!response.ok) {
167
+ throw new OneConnectError("request_failed", `One refused the token request (HTTP ${response.status}).`, response.status);
168
+ }
169
+ return await response.json();
170
+ };
171
+ const toTokens = response => ({
172
+ accessToken: response.access_token,
173
+ refreshToken: response.refresh_token,
174
+ expiresAt: Date.now() + response.expires_in * 1000
175
+ });
176
+ const startAuthorization = (input = {}) => {
177
+ const state = createState();
178
+ const verifier = createPkceVerifier();
179
+ const url = new URL(authorizeUrl);
180
+ url.searchParams.set("client_id", config.clientId);
181
+ url.searchParams.set("redirect_uri", config.redirectUri);
182
+ url.searchParams.set("response_type", "code");
183
+ url.searchParams.set("scope", scopes.join(" "));
184
+ url.searchParams.set("state", state);
185
+ url.searchParams.set("code_challenge", pkceChallenge(verifier));
186
+ url.searchParams.set("code_challenge_method", "S256");
187
+ if (config.permissionSet) url.searchParams.set("permission_set", config.permissionSet);
188
+ if (input.loginHint) url.searchParams.set("login_hint", input.loginHint);
189
+ return {
190
+ redirectUrl: url.toString(),
191
+ cookie: txCookie(state, verifier, config.redirectUri)
192
+ };
193
+ };
194
+ const completeAuthorization = async input => {
195
+ const params = new URL(input.url).searchParams;
196
+ const code = params.get("code");
197
+ const state = params.get("state");
198
+ const oauthError = params.get("error");
199
+ const cookieName = state ? txCookieName(state) : undefined;
200
+ const verifier = cookieName ? input.getCookie(cookieName) : undefined;
201
+ const fail = (outcome, message) => ({
202
+ outcome,
203
+ message,
204
+ redirectUrl: returnUrl("error", message),
205
+ clearCookieName: cookieName
206
+ });
207
+ if (oauthError === "access_denied") return fail("declined", "You cancelled the request.");
208
+ if (oauthError) return fail("failed", `One reported an error: ${oauthError}.`);
209
+ // The returned state names its own cookie. No cookie means a forged
210
+ // or stale state; the code is never exchanged in that case.
211
+ if (!code || !state || !verifier) return fail("failed", "The sign-in attempt expired or was tampered with.");
212
+ try {
213
+ const tokens = toTokens(await exchange(new URLSearchParams({
214
+ grant_type: "authorization_code",
215
+ code,
216
+ redirect_uri: config.redirectUri,
217
+ code_verifier: verifier
218
+ })));
219
+ await tokenStore.saveTokens(input.userId, tokens);
220
+ } catch (error) {
221
+ const status = error instanceof OneConnectError ? error.status : undefined;
222
+ return fail("failed", status ? `One rejected the code exchange (HTTP ${status}).` : "One could not be reached to complete the connection.");
223
+ }
224
+ return {
225
+ outcome: "connected",
226
+ redirectUrl: returnUrl("success"),
227
+ clearCookieName: cookieName
228
+ };
229
+ };
230
+ const refreshTokens = userId => {
231
+ const inFlight = refreshing.get(userId);
232
+ if (inFlight) return inFlight;
233
+ const job = (async () => {
234
+ const current = await tokenStore.loadTokens(userId);
235
+ if (!current) throw new OneConnectError("not_connected", "This user is not connected.");
236
+ try {
237
+ const next = toTokens(await exchange(new URLSearchParams({
238
+ grant_type: "refresh_token",
239
+ refresh_token: current.refreshToken
240
+ })));
241
+ // Both tokens: One rotates the pair on every refresh.
242
+ await tokenStore.saveTokens(userId, next);
243
+ return next;
244
+ } catch (error) {
245
+ // The family is dead: revoked, expired or reused. Keeping the
246
+ // pair would only fail again; the user has to reconnect.
247
+ await tokenStore.clearTokens(userId);
248
+ const status = error instanceof OneConnectError ? error.status : undefined;
249
+ throw new OneConnectError("refresh_failed", "The connection to One has expired or was revoked. Ask the user to connect again.", status);
250
+ } finally {
251
+ refreshing.delete(userId);
252
+ }
253
+ })();
254
+ refreshing.set(userId, job);
255
+ return job;
256
+ };
257
+ const getAccessToken = async userId => {
258
+ const tokens = await tokenStore.loadTokens(userId);
259
+ if (!tokens) throw new OneConnectError("not_connected", "This user is not connected.");
260
+ if (Date.now() < tokens.expiresAt - REFRESH_MARGIN_MS) return tokens.accessToken;
261
+ return (await refreshTokens(userId)).accessToken;
262
+ };
263
+ const oneFetch = async (userId, path, init = {}) => {
264
+ const accessToken = await getAccessToken(userId);
265
+ const headers = new Headers(init.headers);
266
+ headers.set("Authorization", `Bearer ${accessToken}`);
267
+ for (const [name, value] of Object.entries(tenancyHeaders(accessToken))) headers.set(name, value);
268
+ return fetch(`${apiUrl}${path.startsWith("/") ? path : `/${path}`}`, {
269
+ ...init,
270
+ headers
271
+ });
272
+ };
273
+ const listConnections = async userId => {
274
+ const response = await oneFetch(userId, "/connections/reachable");
275
+ if (!response.ok) {
276
+ throw new OneConnectError("request_failed", `One refused the connections request (HTTP ${response.status}).`, response.status);
277
+ }
278
+ const body = await response.json();
279
+ return body.rows ?? [];
280
+ };
281
+ const listActions = async (userId, platform) => {
282
+ const pageUrl = page => `/knowledge?connectionPlatform=${encodeURIComponent(platform)}&limit=${CATALOG_PAGE_SIZE}&page=${page}`;
283
+ const slim = rows => rows.map(row => ({
284
+ _id: String(row._id ?? ""),
285
+ title: String(row.title ?? ""),
286
+ method: String(row.method ?? ""),
287
+ path: String(row.path ?? "")
288
+ }));
289
+ const first = await oneFetch(userId, pageUrl(1));
290
+ if (!first.ok) {
291
+ throw new OneConnectError("request_failed", `One refused the catalog request (HTTP ${first.status}).`, first.status);
292
+ }
293
+ const page1 = await first.json();
294
+ const pages = Math.min(Math.max(page1.pages ?? 1, 1), CATALOG_MAX_PAGES);
295
+ const rest = await Promise.all(Array.from({
296
+ length: pages - 1
297
+ }, (_, index) => oneFetch(userId, pageUrl(index + 2)).then(response => response.ok ? response.json() : null).catch(() => null)));
298
+ return slim([page1, ...rest].flatMap(page => page?.rows ?? []));
299
+ };
300
+ const runAction = async (userId, input) => {
301
+ const method = input.method.toUpperCase();
302
+ const query = input.query ? `?${new URLSearchParams(input.query)}` : "";
303
+ const headers = {
304
+ "x-one-connection-key": input.connectionKey,
305
+ "x-one-action-id": input.actionId
306
+ };
307
+ const hasBody = method !== "GET" && method !== "HEAD" && input.body !== undefined;
308
+ if (hasBody) headers["Content-Type"] = "application/json";
309
+ const response = await oneFetch(userId, `/passthrough${input.path}${query}`, {
310
+ method,
311
+ headers,
312
+ body: hasBody ? JSON.stringify(input.body) : undefined
313
+ });
314
+ const text = await response.text();
315
+ let data = text;
316
+ try {
317
+ data = JSON.parse(text);
318
+ } catch {
319
+ /* the provider answered with something other than JSON */
320
+ }
321
+ // One's own refusals carry a correlationId; a provider's error does
322
+ // not. A One-shaped 403 is the grant working, not the provider.
323
+ const blockedByGrant = !response.ok && typeof data === "object" && data !== null && "correlationId" in data;
324
+ return {
325
+ status: response.status,
326
+ ok: response.ok,
327
+ blockedByGrant,
328
+ data
329
+ };
330
+ };
331
+ return {
332
+ startAuthorization,
333
+ completeAuthorization,
334
+ isConnected: async userId => (await tokenStore.loadTokens(userId)) !== null,
335
+ getAccessToken,
336
+ getTokens: userId => tokenStore.loadTokens(userId),
337
+ refreshTokens,
338
+ disconnect: userId => tokenStore.clearTokens(userId),
339
+ listConnections,
340
+ listActions,
341
+ runAction,
342
+ fetch: oneFetch
343
+ };
344
+ }
345
+
346
+ exports.OneConnectError = OneConnectError;
347
+ exports.createOneConnect = createOneConnect;
348
+ exports.tenancyHeaders = tenancyHeaders;
349
+ exports.tokenScopes = tokenScopes;
@@ -0,0 +1,32 @@
1
+ import { type CompleteAuthorizationInput, type CompleteAuthorizationResult, type OneConnectServerConfig, type OneConnectTokens, type PlatformAction, type ReachableConnection, type RunActionInput, type RunActionResult, type StartAuthorizationInput, type StartAuthorizationResult } from "./types";
2
+ export * from "./types";
3
+ export { tenancyHeaders, tokenScopes } from "./oauth";
4
+ export interface OneConnect {
5
+ /** The authorize leg: where to send the browser and the cookie to set. */
6
+ startAuthorization: (input?: StartAuthorizationInput) => StartAuthorizationResult;
7
+ /** The callback leg: verifies state, exchanges the code, stores the
8
+ * tokens, and says where to send the browser next. Never throws for
9
+ * a failed flow; read `outcome`. */
10
+ completeAuthorization: (input: CompleteAuthorizationInput) => Promise<CompleteAuthorizationResult>;
11
+ /** Whether the user has tokens stored. */
12
+ isConnected: (userId: string) => Promise<boolean>;
13
+ /** A live access token, refreshed first when it is about to expire. */
14
+ getAccessToken: (userId: string) => Promise<string>;
15
+ /** The stored tokens, for display. Null when not connected. */
16
+ getTokens: (userId: string) => Promise<OneConnectTokens | null>;
17
+ /** Forces a refresh now. */
18
+ refreshTokens: (userId: string) => Promise<OneConnectTokens>;
19
+ /** Drops the app's copy of the tokens. The user revokes the grant
20
+ * itself from their One dashboard. */
21
+ disconnect: (userId: string) => Promise<void>;
22
+ /** The connections the grant reaches, each with its access. */
23
+ listConnections: (userId: string) => Promise<ReachableConnection[]>;
24
+ /** Every catalog action of a platform. What exists, not what is
25
+ * permitted; the grant decides that when the action runs. */
26
+ listActions: (userId: string, platform: string) => Promise<PlatformAction[]>;
27
+ /** Runs one action through One with the grant. */
28
+ runAction: (userId: string, input: RunActionInput) => Promise<RunActionResult>;
29
+ /** Any authenticated request to One's /v1 API, headers handled. */
30
+ fetch: (userId: string, path: string, init?: RequestInit) => Promise<Response>;
31
+ }
32
+ export declare function createOneConnect(config: OneConnectServerConfig): OneConnect;