@clearly-hub/browser 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  Connect your web app to Clearly.HUB with a script, some config and one server handler. You get
4
4
  "Sign in with Clearly.HUB", the hosted screens (choose a plan, pick data, save a result, choose a
5
- hub) and each hub's subscription to your app. Tokens stay on your server.
5
+ hub) and each hub's subscription to your app. The refresh token stays on your server; the page gets
6
+ the access token only when it asks, for protected data.
6
7
 
7
8
  > **Preview (0.x).** The API may still change between minor versions. Proprietary: see
8
9
  > [LICENSE](LICENSE).
@@ -51,7 +52,9 @@ Hono, Bun or Deno. Mount it at `/clearly-hub/*`.
51
52
 
52
53
  <script type="module">
53
54
  const session = await clearlyHub.session(); // { signedIn, user }
54
- window.addEventListener('clearly-hub-answer', (event) => console.log(event.detail));
55
+ window.addEventListener('clearly-hub-answer', (event) =>
56
+ console.log(event.detail),
57
+ );
55
58
  </script>
56
59
  ```
57
60
 
@@ -59,14 +62,16 @@ Or `npm install @clearly-hub/browser` and `import { clearlyHub } from '@clearly-
59
62
 
60
63
  ## The browser package
61
64
 
62
- | | |
63
- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
64
- | `clearlyHub.session()` | `{ signedIn: false }`, or the signed-in user (`sub`, `email`, `name`, `appUserId`) |
65
- | `clearlyHub.signIn()`, `signOut()` | Go through Clearly.HUB and come back to this page |
66
- | `clearlyHub.open(action, { data, display })` | Opens a hosted screen; resolves with its checked answer. Call it from a click: browsers only allow popups there |
67
- | `clearlyHub.result()` | After a redirect screen: its answer, on the page it came back to |
68
- | `<clearly-hub-button action display data label>` | Branded button for `sign-in`, `sign-out` and each screen |
69
- | `clearly-hub-answer` event | Every answer, from a popup or after a redirect |
65
+ | | |
66
+ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
67
+ | `clearlyHub.session()` | `{ signedIn: false }`, or the signed-in user (`sub`, `email`, `name`, `appUserId`) |
68
+ | `clearlyHub.signIn()`, `signOut()` | Go through Clearly.HUB and come back to this page |
69
+ | `clearlyHub.open(action, { data, display })` | Opens a hosted screen; resolves with its checked answer. Call it from a click: browsers only allow popups there |
70
+ | `clearlyHub.result()` | After a redirect screen: its answer, on the page it came back to |
71
+ | `clearlyHub.getToken()` | The person's key for protected and paid data, or `null` when signed out. See [Protected data](#protected-data) |
72
+ | `clearlyHub.onAnswer(callback)` | Every answer, once each, from a popup or after a redirect; also when you start listening after it arrived (React). Returns a function that stops listening |
73
+ | `<clearly-hub-button action display data label>` | Branded button for `sign-in`, `sign-out` and each screen |
74
+ | `clearly-hub-answer` event | Every answer, from a popup or after a redirect |
70
75
 
71
76
  **Styling the button.** It uses your page's font. Attributes for the common cases:
72
77
  `variant="outline"`, `size="small"` or `size="large"`. CSS custom properties for your look:
@@ -93,15 +98,33 @@ blocked popup falls back to a redirect.
93
98
  | `REGISTER_RESOURCE` | `{ url, format }` of your result, at a public https URL | `activeHub`, `dataset`, `resource` |
94
99
  | `SELECT_HUB` | | `hub` (`_id`, `name`) |
95
100
 
101
+ ## Protected data
102
+
103
+ A protected or paid resource from `DATA_PICKER` (`requiresToken: true`) has a `url` on Clearly.HUB,
104
+ which checks the person's key on every request. Send it as the `Authorization` header. In the page:
105
+
106
+ ```js
107
+ const response = await fetch(resource.url, {
108
+ headers: { Authorization: await clearlyHub.getToken() },
109
+ });
110
+ ```
111
+
112
+ Map layers (WMS, WMTS, tiles) load many URLs themselves: use a Leaflet, OpenLayers or Cesium option
113
+ or plugin that adds headers to each request. The key lasts an hour; `getToken()` gets a new one in
114
+ time, so call it again rather than keeping the value. On your server, `hub.fetchAsUser(req, url)`
115
+ does the same.
116
+
117
+ The key is the person's own Clearly.HUB key: keep scripts you don't trust off pages that use it.
118
+
96
119
  ## The Node package
97
120
 
98
121
  | | |
99
122
  | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
- | `hub.express()`, `hub.handler` | The routes: `/sign-in`, `/callback`, `/sign-out`, `/session`, `/open`, `/answer`, `/result` |
123
+ | `hub.express()`, `hub.handler` | The routes: `/sign-in`, `/callback`, `/sign-out`, `/session`, `/open`, `/answer`, `/result`, `/token` |
101
124
  | `onSignIn(person, { request, req })` | Called after each sign-in. Return your own user id to keep it in the session, or `{ appUserId, redirectTo }` to also choose the next page |
102
125
  | `onAnswer(result, { user, data, request, req })` | Called with each checked answer before it reaches the browser, for example to set up a workspace. `data` is what the screen was sent; what you return reaches the page as `result.app` |
103
126
  | `hub.getUser(req)` | Who's signed in, in your own routes |
104
- | `hub.fetchAsUser(req, url)` | Fetch protected or paid data (from `DATA_PICKER`) with the person's key |
127
+ | `hub.fetchAsUser(req, url)` | Fetch protected or paid data (from `DATA_PICKER`) on your server, with the person's key |
105
128
  | `hub.getSubscription(hubId)`, `getSubscriptions()` | A hub's subscription to your app, or all of them |
106
129
  | `hub.reportUsage({ request or sub, hubId, quantity })` | Report billable use; Clearly.HUB bills the hub. Pass `sub` for a Clearly.HUB account you stored, without a live sign-in |
107
130
  | `hub.getUserToken(req)`, `getAppToken()` | The person's key and your app's key, for calls of your own |
@@ -131,7 +154,10 @@ store; it's split over several cookies because tokens are large.
131
154
 
132
155
  - Setting up your own workspaces or tenants: do it by hand, with plans that need approval, or in
133
156
  `onAnswer` for `SELECT_SUBSCRIPTION`.
134
- - Other server languages: follow the Build checklist.
157
+ - Other server languages (.NET, Python, ...): the browser package works with any
158
+ backend that answers the same routes. Contact us at
159
+ [support@futureinsight.nl](mailto:support@futureinsight.nl) and we'll help
160
+ you with yours.
135
161
 
136
162
  ## Working on this repo
137
163
 
@@ -1,4 +1,4 @@
1
- "use strict";(()=>{var C=Object.defineProperty;var T=(r,t,n)=>t in r?C(r,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):r[t]=n;var l=(r,t,n)=>T(r,typeof t!="symbol"?t+"":t,n);var x=["SELECT_SUBSCRIPTION","DATA_PICKER","REGISTER_RESOURCE","SELECT_HUB"];function b(r){return x.includes(r)}function E(r,t){if(r!=="REGISTER_RESOURCE")return;let{url:n,format:i}=t??{};if(typeof n!="string"||!n||typeof i!="string"||!i)return'REGISTER_RESOURCE needs data with a url and a format, for example { url, format: "geojson" }';let o;try{o=new URL(n).hostname}catch{return`REGISTER_RESOURCE: "${n}" is not a URL`}if(!o.includes("."))return"REGISTER_RESOURCE needs a url with a domain Clearly.HUB accepts, not localhost"}var S="/clearly-hub";var c={signIn:"/sign-in",callback:"/callback",signOut:"/sign-out",session:"/session",open:"/open",answer:"/answer",result:"/result"},y="clearly-hub-answers";var d="clearly-hub-answer",R=480,h=class{constructor(t={}){l(this,"basePath",S);l(this,"display","popup");l(this,"redirectResult");this.configure(t)}configure(t){return t.basePath&&(this.basePath=t.basePath.replace(/\/+$/,"")),t.display&&(this.display=t.display),this}async session(){let t=await fetch(this.url(c.session),{credentials:"same-origin"});if(!t.ok)throw new Error(`Clearly.HUB session failed (${t.status})`);return t.json()}signIn({returnTo:t=m()}={}){window.location.assign(this.url(c.signIn,{returnTo:t}))}signOut(){window.location.assign(this.url(c.signOut))}open(t,n={}){let i=E(t,n.data);if(i)return Promise.reject(new Error(`Clearly.HUB: ${i}`));let o=crypto.randomUUID(),e=n.display??this.display,s={action:t,ref:o,returnTo:n.returnTo??m(),data:n.data?JSON.stringify(n.data):void 0};if(e==="popup"){let a=window.open(this.url(c.open,{...s,display:"popup"}),"clearly-hub-screen",`width=${R},height=${screen.availHeight},left=${screen.availWidth-R}`);if(a)return this.waitForPopup(a,o,t,n.eventTarget??window)}return window.location.assign(this.url(c.open,{...s,display:"redirect"})),new Promise(()=>{})}result(){return this.redirectResult??(this.redirectResult=fetch(this.url(c.result),{credentials:"same-origin"}).then(t=>t.ok?t.json():null).then(t=>t?.result??null).catch(()=>null)),this.redirectResult}waitForPopup(t,n,i,o){return new Promise(e=>{let s=new BroadcastChannel(y),a=!1,f=u=>{a||(a=!0,s.close(),window.clearInterval(g),o.dispatchEvent(new CustomEvent(d,{detail:u,bubbles:!0,composed:!0})),e(u))};s.onmessage=u=>{u.data?.ref===n&&f(u.data.result)};let g=window.setInterval(()=>{t.closed&&(window.clearInterval(g),window.setTimeout(()=>f({action:i,result:"CANCELLED"}),500))},400)})}url(t,n={}){let i=new URLSearchParams;for(let[e,s]of Object.entries(n))s!==void 0&&i.set(e,s);let o=i.toString();return`${this.basePath}${t}${o?`?${o}`:""}`}};function m(){return`${window.location.pathname}${window.location.search}${window.location.hash}`}var A={"sign-in":"Sign in with Clearly.HUB","sign-out":"Sign out",SELECT_SUBSCRIPTION:"Choose a plan in Clearly.HUB",DATA_PICKER:"Pick data from Clearly.HUB",REGISTER_RESOURCE:"Save to Clearly.HUB",SELECT_HUB:"Choose a hub"},O=`
1
+ "use strict";(()=>{var C=Object.defineProperty;var T=(r,e,n)=>e in r?C(r,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):r[e]=n;var a=(r,e,n)=>T(r,typeof e!="symbol"?e+"":e,n);var E="/clearly-hub";var l={signIn:"/sign-in",callback:"/callback",signOut:"/sign-out",session:"/session",open:"/open",answer:"/answer",result:"/result",token:"/token"},R="clearly-hub-answers";var x=["SELECT_SUBSCRIPTION","DATA_PICKER","REGISTER_RESOURCE","SELECT_HUB"];function S(r){return x.includes(r)}function b(r,e){if(r!=="REGISTER_RESOURCE")return;let{url:n,format:s}=e??{};if(typeof n!="string"||!n||typeof s!="string"||!s)return'REGISTER_RESOURCE needs data with a url and a format, for example { url, format: "geojson" }';let o;try{o=new URL(n).hostname}catch{return`REGISTER_RESOURCE: "${n}" is not a URL`}if(!o.includes("."))return"REGISTER_RESOURCE needs a url with a domain Clearly.HUB accepts, not localhost"}var u="clearly-hub-answer",y=480,h=class{constructor(e={}){a(this,"basePath",E);a(this,"display","popup");a(this,"redirectResult");a(this,"unclaimedRedirectResult");a(this,"token");a(this,"tokenRequest");this.configure(e)}configure(e){return e.basePath&&(this.basePath=e.basePath.replace(/\/+$/,"")),e.display&&(this.display=e.display),this}async session(){let e=await fetch(this.url(l.session),{credentials:"same-origin"});if(!e.ok)throw new Error(`Clearly.HUB session failed (${e.status})`);return e.json()}async getToken(){return this.token&&this.token.expiresAt>Date.now()+6e4?this.token.value:(this.tokenRequest??(this.tokenRequest=fetch(this.url(l.token),{credentials:"same-origin"}).then(async e=>{if(e.status===401)return null;if(!e.ok)throw new Error(`Clearly.HUB token failed (${e.status})`);let{accessToken:n,expiresIn:s}=await e.json();return{value:n,expiresAt:Date.now()+s*1e3}}).finally(()=>{this.tokenRequest=void 0})),this.token=await this.tokenRequest??void 0,this.token?.value??null)}signIn({returnTo:e=m()}={}){window.location.assign(this.url(l.signIn,{returnTo:e}))}signOut(){window.location.assign(this.url(l.signOut))}open(e,n={}){let s=b(e,n.data);if(s)return Promise.reject(new Error(`Clearly.HUB: ${s}`));let o=crypto.randomUUID(),t=n.display??this.display,i={action:e,ref:o,returnTo:n.returnTo??m(),data:n.data?JSON.stringify(n.data):void 0};if(t==="popup"){let c=window.open(this.url(l.open,{...i,display:"popup"}),"clearly-hub-screen",`width=${y},height=${screen.availHeight},left=${screen.availWidth-y}`);if(c)return this.waitForPopup(c,o,e,n.eventTarget??window)}return window.location.assign(this.url(l.open,{...i,display:"redirect"})),new Promise(()=>{})}onAnswer(e){let n=o=>{let t=o.detail;t===this.unclaimedRedirectResult&&(this.unclaimedRedirectResult=void 0),e(t)};window.addEventListener(u,n);let s=this.unclaimedRedirectResult;return s&&(this.unclaimedRedirectResult=void 0,e(s)),()=>window.removeEventListener(u,n)}deliverRedirectResult(e){this.unclaimedRedirectResult=e,window.dispatchEvent(new CustomEvent(u,{detail:e}))}result(){return this.redirectResult??(this.redirectResult=fetch(this.url(l.result),{credentials:"same-origin"}).then(e=>e.ok?e.json():null).then(e=>e?.result??null).catch(()=>null)),this.redirectResult}waitForPopup(e,n,s,o){return new Promise(t=>{let i=new BroadcastChannel(R),c=!1,f=p=>{c||(c=!0,i.close(),window.clearInterval(g),o.dispatchEvent(new CustomEvent(u,{detail:p,bubbles:!0,composed:!0})),t(p))};i.onmessage=p=>{p.data?.ref===n&&f(p.data.result)};let g=window.setInterval(()=>{e.closed&&(window.clearInterval(g),window.setTimeout(()=>f({action:s,result:"CANCELLED"}),500))},400)})}url(e,n={}){let s=new URLSearchParams;for(let[t,i]of Object.entries(n))i!==void 0&&s.set(t,i);let o=s.toString();return`${this.basePath}${e}${o?`?${o}`:""}`}};function m(){return`${window.location.pathname}${window.location.search}${window.location.hash}`}var w={"sign-in":"Sign in with Clearly.HUB","sign-out":"Sign out",SELECT_SUBSCRIPTION:"Choose a plan in Clearly.HUB",DATA_PICKER:"Pick data from Clearly.HUB",REGISTER_RESOURCE:"Save to Clearly.HUB",SELECT_HUB:"Choose a hub"},k=`
2
2
  :host { display: inline-block; }
3
3
  button {
4
4
  --color: var(--clearly-hub-color, #1fa69d);
@@ -20,4 +20,4 @@
20
20
  button:focus-visible { outline: 2px solid var(--color); outline-offset: 2px; }
21
21
  button:disabled { opacity: 0.6; cursor: default; }
22
22
  svg { width: 1.25em; height: 1.25em; flex: none; fill: currentColor; }
23
- `,U='<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 6.5c-1.66 0-3 1.34-3 3 0 .07 0 .14.01.21l-2.03.68c-.64-1.21-1.82-2.09-3.22-2.32V5.91C14.04 5.57 15 4.4 15 3c0-1.66-1.34-3-3-3S9 1.34 9 3c0 1.4.96 2.57 2.25 2.91v2.16c-1.4.23-2.58 1.11-3.22 2.32l-2.04-.68C6 9.64 6 9.57 6 9.5c0-1.66-1.34-3-3-3s-3 1.34-3 3 1.34 3 3 3c1.06 0 1.98-.55 2.52-1.37l2.03.68c-.2 1.29.17 2.66 1.09 3.69l-1.41 1.77Q6.66 17 6 17c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3c0-.68-.22-1.3-.6-1.8l1.41-1.77c1.36.76 3.02.75 4.37 0l1.41 1.77c-.37.5-.59 1.12-.59 1.8 0 1.66 1.34 3 3 3s3-1.34 3-3-1.34-3-3-3c-.44 0-.85.09-1.23.26l-1.41-1.77c.93-1.04 1.29-2.4 1.09-3.69l2.03-.68c.53.82 1.46 1.37 2.52 1.37 1.66 0 3-1.34 3-3S22.66 6.5 21 6.5m-18 4c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1M6 21c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1m5-18c0-.55.45-1 1-1s1 .45 1 1-.45 1-1 1-1-.45-1-1m1 12c-1.38 0-2.5-1.12-2.5-2.5S10.62 10 12 10s2.5 1.12 2.5 2.5S13.38 15 12 15m6 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1m3-8.5c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1"/></svg>';function w(r,t="clearly-hub-button"){if(typeof customElements>"u"||customElements.get(t))return;class n extends HTMLElement{constructor(){super();l(this,"button");let e=this.attachShadow({mode:"open"});e.innerHTML=`<style>${O}</style><button part="button" type="button"></button>`,this.button=e.querySelector("button"),this.button.addEventListener("click",()=>this.run())}connectedCallback(){for(let e of["action","display","label","variant","size","data"])if(Object.prototype.hasOwnProperty.call(this,e)){let s=this[e];delete this[e],this[e]=s}this.render()}attributeChangedCallback(){this.render()}get action(){let e=this.getAttribute("action")??"sign-in";return e in A?e:"sign-in"}set action(e){this.setOrRemove("action",e)}get display(){return this.getAttribute("display")==="redirect"?"redirect":void 0}set display(e){this.setOrRemove("display",e)}get label(){return this.getAttribute("label")}set label(e){this.setOrRemove("label",e)}get variant(){return this.getAttribute("variant")}set variant(e){this.setOrRemove("variant",e)}get size(){return this.getAttribute("size")}set size(e){this.setOrRemove("size",e)}get data(){let e=this.getAttribute("data");return e?JSON.parse(e):void 0}set data(e){this.setOrRemove("data",typeof e=="string"||e==null?e:JSON.stringify(e))}setOrRemove(e,s){s==null?this.removeAttribute(e):this.setAttribute(e,s)}render(){let e=this.label??A[this.action];this.button.innerHTML=`${U}<span></span>`,this.button.querySelector("span").textContent=e}async run(){let e=this.action;if(e==="sign-in")return r.signIn();if(e==="sign-out")return r.signOut();if(b(e)){this.button.disabled=!0;try{await r.open(e,{display:this.display,data:this.data,eventTarget:this})}catch(s){console.error(s);let a={action:e,result:"FAILURE",error:s.message};this.dispatchEvent(new CustomEvent(d,{detail:a,bubbles:!0,composed:!0}))}finally{this.button.disabled=!1}}}}l(n,"observedAttributes",["action","label"]),customElements.define(t,n)}var p=new h;w(p);typeof window<"u"&&queueMicrotask(()=>{p.result().then(r=>{r&&window.dispatchEvent(new CustomEvent(d,{detail:r}))})});var v=document.currentScript;p.configure({basePath:v?.dataset.basePath,display:v?.dataset.display==="redirect"?"redirect":void 0});window.clearlyHub=p;})();
23
+ `,O='<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 6.5c-1.66 0-3 1.34-3 3 0 .07 0 .14.01.21l-2.03.68c-.64-1.21-1.82-2.09-3.22-2.32V5.91C14.04 5.57 15 4.4 15 3c0-1.66-1.34-3-3-3S9 1.34 9 3c0 1.4.96 2.57 2.25 2.91v2.16c-1.4.23-2.58 1.11-3.22 2.32l-2.04-.68C6 9.64 6 9.57 6 9.5c0-1.66-1.34-3-3-3s-3 1.34-3 3 1.34 3 3 3c1.06 0 1.98-.55 2.52-1.37l2.03.68c-.2 1.29.17 2.66 1.09 3.69l-1.41 1.77Q6.66 17 6 17c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3c0-.68-.22-1.3-.6-1.8l1.41-1.77c1.36.76 3.02.75 4.37 0l1.41 1.77c-.37.5-.59 1.12-.59 1.8 0 1.66 1.34 3 3 3s3-1.34 3-3-1.34-3-3-3c-.44 0-.85.09-1.23.26l-1.41-1.77c.93-1.04 1.29-2.4 1.09-3.69l2.03-.68c.53.82 1.46 1.37 2.52 1.37 1.66 0 3-1.34 3-3S22.66 6.5 21 6.5m-18 4c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1M6 21c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1m5-18c0-.55.45-1 1-1s1 .45 1 1-.45 1-1 1-1-.45-1-1m1 12c-1.38 0-2.5-1.12-2.5-2.5S10.62 10 12 10s2.5 1.12 2.5 2.5S13.38 15 12 15m6 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1m3-8.5c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1"/></svg>';function A(r,e="clearly-hub-button"){if(typeof customElements>"u"||customElements.get(e))return;class n extends HTMLElement{constructor(){super();a(this,"button");let t=this.attachShadow({mode:"open"});t.innerHTML=`<style>${k}</style><button part="button" type="button"></button>`,this.button=t.querySelector("button"),this.button.addEventListener("click",()=>this.run())}connectedCallback(){for(let t of["action","display","label","variant","size","data"])if(Object.prototype.hasOwnProperty.call(this,t)){let i=this[t];delete this[t],this[t]=i}this.render()}attributeChangedCallback(){this.render()}get action(){let t=this.getAttribute("action")??"sign-in";return t in w?t:"sign-in"}set action(t){this.setOrRemove("action",t)}get display(){return this.getAttribute("display")==="redirect"?"redirect":void 0}set display(t){this.setOrRemove("display",t)}get label(){return this.getAttribute("label")}set label(t){this.setOrRemove("label",t)}get variant(){return this.getAttribute("variant")}set variant(t){this.setOrRemove("variant",t)}get size(){return this.getAttribute("size")}set size(t){this.setOrRemove("size",t)}get data(){let t=this.getAttribute("data");return t?JSON.parse(t):void 0}set data(t){this.setOrRemove("data",typeof t=="string"||t==null?t:JSON.stringify(t))}setOrRemove(t,i){i==null?this.removeAttribute(t):this.setAttribute(t,i)}render(){let t=this.label??w[this.action];this.button.innerHTML=`${O}<span></span>`,this.button.querySelector("span").textContent=t}async run(){let t=this.action;if(t==="sign-in")return r.signIn();if(t==="sign-out")return r.signOut();if(S(t)){this.button.disabled=!0;try{await r.open(t,{display:this.display,data:this.data,eventTarget:this})}catch(i){console.error(i);let c={action:t,result:"FAILURE",error:i.message};this.dispatchEvent(new CustomEvent(u,{detail:c,bubbles:!0,composed:!0}))}finally{this.button.disabled=!1}}}}a(n,"observedAttributes",["action","label"]),customElements.define(e,n)}var d=new h;A(d);if(typeof window<"u"){let r=()=>{d.result().then(e=>{e&&d.deliverRedirectResult(e)})};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",r,{once:!0}):queueMicrotask(r)}var v=document.currentScript;d.configure({basePath:v?.dataset.basePath,display:v?.dataset.display==="redirect"?"redirect":void 0});window.clearlyHub=d;})();
package/dist/index.d.ts CHANGED
@@ -1,6 +1,26 @@
1
1
  import { S as ScreenResult, a as ScreenDisplay, b as ScreenAction, c as ScreenData } from './screens-CZiYNLZI.js';
2
2
  export { H as HubRef, P as PickedResource, d as SCREEN_ACTIONS, e as ScreenAnswers, f as checkScreenData, i as isScreenAction } from './screens-CZiYNLZI.js';
3
3
 
4
+ declare const DEFAULT_BASE_PATH = "/clearly-hub";
5
+ declare const DEFAULT_HUB_URL = "https://hub.clearly.app";
6
+ declare const DEFAULT_AUTH_URL = "https://auth.clearly.app";
7
+ declare const DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
8
+ declare const ROUTES: {
9
+ readonly signIn: "/sign-in";
10
+ readonly callback: "/callback";
11
+ readonly signOut: "/sign-out";
12
+ readonly session: "/session";
13
+ readonly open: "/open";
14
+ readonly answer: "/answer";
15
+ readonly result: "/result";
16
+ readonly token: "/token";
17
+ };
18
+ declare const ANSWER_CHANNEL = "clearly-hub-answers";
19
+ interface AnswerMessage {
20
+ ref: string;
21
+ result: ScreenResult;
22
+ }
23
+
4
24
  interface SessionUser {
5
25
  sub: string;
6
26
  email: string;
@@ -19,24 +39,9 @@ type Session = {
19
39
  signedIn: true;
20
40
  user: SessionUser;
21
41
  };
22
-
23
- declare const DEFAULT_BASE_PATH = "/clearly-hub";
24
- declare const DEFAULT_HUB_URL = "https://hub.clearly.app";
25
- declare const DEFAULT_AUTH_URL = "https://auth.clearly.app";
26
- declare const DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
27
- declare const ROUTES: {
28
- readonly signIn: "/sign-in";
29
- readonly callback: "/callback";
30
- readonly signOut: "/sign-out";
31
- readonly session: "/session";
32
- readonly open: "/open";
33
- readonly answer: "/answer";
34
- readonly result: "/result";
35
- };
36
- declare const ANSWER_CHANNEL = "clearly-hub-answers";
37
- interface AnswerMessage {
38
- ref: string;
39
- result: ScreenResult;
42
+ interface AccessToken {
43
+ accessToken: string;
44
+ expiresIn: number;
40
45
  }
41
46
 
42
47
  interface ClientOptions {
@@ -54,14 +59,20 @@ declare class ClearlyHubClient {
54
59
  private basePath;
55
60
  private display;
56
61
  private redirectResult?;
62
+ private unclaimedRedirectResult?;
63
+ private token?;
64
+ private tokenRequest?;
57
65
  constructor(options?: ClientOptions);
58
66
  configure(options: ClientOptions): this;
59
67
  session(): Promise<Session>;
68
+ getToken(): Promise<string | null>;
60
69
  signIn({ returnTo }?: {
61
70
  returnTo?: string;
62
71
  }): void;
63
72
  signOut(): void;
64
73
  open<A extends ScreenAction>(action: A, options?: OpenOptions<A>): Promise<ScreenResult<A>>;
74
+ onAnswer(callback: (result: ScreenResult) => void): () => void;
75
+ deliverRedirectResult(result: ScreenResult): void;
65
76
  result(): Promise<ScreenResult | null>;
66
77
  private waitForPopup;
67
78
  private url;
@@ -72,4 +83,4 @@ declare function defineButton(client: ClearlyHubClient, tagName?: string): void;
72
83
  declare const clearlyHub: ClearlyHubClient;
73
84
 
74
85
  export { ANSWER_CHANNEL, ANSWER_EVENT, ClearlyHubClient, DEFAULT_AUTH_URL, DEFAULT_BASE_PATH, DEFAULT_HUB_URL, DEFAULT_ISSUER, ROUTES, ScreenAction, ScreenData, ScreenDisplay, ScreenResult, clearlyHub, defineButton };
75
- export type { AnswerMessage, ClientOptions, HubSubscription, OpenOptions, Session, SessionUser };
86
+ export type { AccessToken, AnswerMessage, ClientOptions, HubSubscription, OpenOptions, Session, SessionUser };
package/dist/index.js CHANGED
@@ -2,6 +2,20 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
+ // ../shared/src/protocol.ts
6
+ var DEFAULT_BASE_PATH = "/clearly-hub";
7
+ var ROUTES = {
8
+ signIn: "/sign-in",
9
+ callback: "/callback",
10
+ signOut: "/sign-out",
11
+ session: "/session",
12
+ open: "/open",
13
+ answer: "/answer",
14
+ result: "/result",
15
+ token: "/token"
16
+ };
17
+ var ANSWER_CHANNEL = "clearly-hub-answers";
18
+
5
19
  // ../shared/src/screens.ts
6
20
  var SCREEN_ACTIONS = [
7
21
  "SELECT_SUBSCRIPTION",
@@ -30,19 +44,6 @@ function checkScreenData(action, data) {
30
44
  return void 0;
31
45
  }
32
46
 
33
- // ../shared/src/protocol.ts
34
- var DEFAULT_BASE_PATH = "/clearly-hub";
35
- var ROUTES = {
36
- signIn: "/sign-in",
37
- callback: "/callback",
38
- signOut: "/sign-out",
39
- session: "/session",
40
- open: "/open",
41
- answer: "/answer",
42
- result: "/result"
43
- };
44
- var ANSWER_CHANNEL = "clearly-hub-answers";
45
-
46
47
  // src/client.ts
47
48
  var ANSWER_EVENT = "clearly-hub-answer";
48
49
  var POPUP_WIDTH = 480;
@@ -51,6 +52,11 @@ var ClearlyHubClient = class {
51
52
  __publicField(this, "basePath", DEFAULT_BASE_PATH);
52
53
  __publicField(this, "display", "popup");
53
54
  __publicField(this, "redirectResult");
55
+ // An answer after a redirect that no onAnswer() callback has had yet: the
56
+ // first callback to come gets it
57
+ __publicField(this, "unclaimedRedirectResult");
58
+ __publicField(this, "token");
59
+ __publicField(this, "tokenRequest");
54
60
  this.configure(options);
55
61
  }
56
62
  configure(options) {
@@ -60,10 +66,37 @@ var ClearlyHubClient = class {
60
66
  }
61
67
  // Who's signed in. Which of your tenants they work for is your app's to know
62
68
  async session() {
63
- const response = await fetch(this.url(ROUTES.session), { credentials: "same-origin" });
64
- if (!response.ok) throw new Error(`Clearly.HUB session failed (${response.status})`);
69
+ const response = await fetch(this.url(ROUTES.session), {
70
+ credentials: "same-origin"
71
+ });
72
+ if (!response.ok)
73
+ throw new Error(`Clearly.HUB session failed (${response.status})`);
65
74
  return response.json();
66
75
  }
76
+ // The person's Clearly.HUB key, for protected and paid data (a resource's
77
+ // url from DATA_PICKER): send it as the Authorization header, for example
78
+ // from your map layers. Null when nobody's signed in. Kept until a minute
79
+ // before it expires, so ask again for each use instead of storing it.
80
+ async getToken() {
81
+ if (this.token && this.token.expiresAt > Date.now() + 6e4)
82
+ return this.token.value;
83
+ this.tokenRequest ?? (this.tokenRequest = fetch(this.url(ROUTES.token), {
84
+ credentials: "same-origin"
85
+ }).then(async (response) => {
86
+ if (response.status === 401) return null;
87
+ if (!response.ok)
88
+ throw new Error(`Clearly.HUB token failed (${response.status})`);
89
+ const { accessToken, expiresIn } = await response.json();
90
+ return {
91
+ value: accessToken,
92
+ expiresAt: Date.now() + expiresIn * 1e3
93
+ };
94
+ }).finally(() => {
95
+ this.tokenRequest = void 0;
96
+ }));
97
+ this.token = await this.tokenRequest ?? void 0;
98
+ return this.token?.value ?? null;
99
+ }
67
100
  signIn({ returnTo = currentPath() } = {}) {
68
101
  window.location.assign(this.url(ROUTES.signIn, { returnTo }));
69
102
  }
@@ -75,7 +108,8 @@ var ClearlyHubClient = class {
75
108
  // back to a redirect.
76
109
  open(action, options = {}) {
77
110
  const dataProblem = checkScreenData(action, options.data);
78
- if (dataProblem) return Promise.reject(new Error(`Clearly.HUB: ${dataProblem}`));
111
+ if (dataProblem)
112
+ return Promise.reject(new Error(`Clearly.HUB: ${dataProblem}`));
79
113
  const ref = crypto.randomUUID();
80
114
  const display = options.display ?? this.display;
81
115
  const query = {
@@ -90,16 +124,50 @@ var ClearlyHubClient = class {
90
124
  "clearly-hub-screen",
91
125
  `width=${POPUP_WIDTH},height=${screen.availHeight},left=${screen.availWidth - POPUP_WIDTH}`
92
126
  );
93
- if (popup) return this.waitForPopup(popup, ref, action, options.eventTarget ?? window);
127
+ if (popup)
128
+ return this.waitForPopup(
129
+ popup,
130
+ ref,
131
+ action,
132
+ options.eventTarget ?? window
133
+ );
94
134
  }
95
- window.location.assign(this.url(ROUTES.open, { ...query, display: "redirect" }));
135
+ window.location.assign(
136
+ this.url(ROUTES.open, { ...query, display: "redirect" })
137
+ );
96
138
  return new Promise(() => {
97
139
  });
98
140
  }
141
+ // Every answer, once each: from a popup, and after a redirect, also when
142
+ // that one arrived before you called this (as in React, where listeners
143
+ // come after the first render). Returns a function that stops listening.
144
+ onAnswer(callback) {
145
+ const listener = (event) => {
146
+ const result = event.detail;
147
+ if (result === this.unclaimedRedirectResult)
148
+ this.unclaimedRedirectResult = void 0;
149
+ callback(result);
150
+ };
151
+ window.addEventListener(ANSWER_EVENT, listener);
152
+ const unclaimed = this.unclaimedRedirectResult;
153
+ if (unclaimed) {
154
+ this.unclaimedRedirectResult = void 0;
155
+ callback(unclaimed);
156
+ }
157
+ return () => window.removeEventListener(ANSWER_EVENT, listener);
158
+ }
159
+ // On page load: announces the answer after a redirect as the
160
+ // clearly-hub-answer event, and keeps it for an onAnswer() that comes later
161
+ deliverRedirectResult(result) {
162
+ this.unclaimedRedirectResult = result;
163
+ window.dispatchEvent(new CustomEvent(ANSWER_EVENT, { detail: result }));
164
+ }
99
165
  // After a redirect screen: its answer, once, on the page it came back to.
100
166
  // Null when there is none.
101
167
  result() {
102
- this.redirectResult ?? (this.redirectResult = fetch(this.url(ROUTES.result), { credentials: "same-origin" }).then((response) => response.ok ? response.json() : null).then((message) => message?.result ?? null).catch(() => null));
168
+ this.redirectResult ?? (this.redirectResult = fetch(this.url(ROUTES.result), {
169
+ credentials: "same-origin"
170
+ }).then((response) => response.ok ? response.json() : null).then((message) => message?.result ?? null).catch(() => null));
103
171
  return this.redirectResult;
104
172
  }
105
173
  waitForPopup(popup, ref, action, eventTarget) {
@@ -112,12 +180,17 @@ var ClearlyHubClient = class {
112
180
  channel.close();
113
181
  window.clearInterval(closedCheck);
114
182
  eventTarget.dispatchEvent(
115
- new CustomEvent(ANSWER_EVENT, { detail: result, bubbles: true, composed: true })
183
+ new CustomEvent(ANSWER_EVENT, {
184
+ detail: result,
185
+ bubbles: true,
186
+ composed: true
187
+ })
116
188
  );
117
189
  resolve(result);
118
190
  };
119
191
  channel.onmessage = (event) => {
120
- if (event.data?.ref === ref) finish(event.data.result);
192
+ if (event.data?.ref === ref)
193
+ finish(event.data.result);
121
194
  };
122
195
  const closedCheck = window.setInterval(() => {
123
196
  if (popup.closed) {
@@ -173,7 +246,8 @@ var STYLE = `
173
246
  `;
174
247
  var ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 6.5c-1.66 0-3 1.34-3 3 0 .07 0 .14.01.21l-2.03.68c-.64-1.21-1.82-2.09-3.22-2.32V5.91C14.04 5.57 15 4.4 15 3c0-1.66-1.34-3-3-3S9 1.34 9 3c0 1.4.96 2.57 2.25 2.91v2.16c-1.4.23-2.58 1.11-3.22 2.32l-2.04-.68C6 9.64 6 9.57 6 9.5c0-1.66-1.34-3-3-3s-3 1.34-3 3 1.34 3 3 3c1.06 0 1.98-.55 2.52-1.37l2.03.68c-.2 1.29.17 2.66 1.09 3.69l-1.41 1.77Q6.66 17 6 17c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3c0-.68-.22-1.3-.6-1.8l1.41-1.77c1.36.76 3.02.75 4.37 0l1.41 1.77c-.37.5-.59 1.12-.59 1.8 0 1.66 1.34 3 3 3s3-1.34 3-3-1.34-3-3-3c-.44 0-.85.09-1.23.26l-1.41-1.77c.93-1.04 1.29-2.4 1.09-3.69l2.03-.68c.53.82 1.46 1.37 2.52 1.37 1.66 0 3-1.34 3-3S22.66 6.5 21 6.5m-18 4c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1M6 21c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1m5-18c0-.55.45-1 1-1s1 .45 1 1-.45 1-1 1-1-.45-1-1m1 12c-1.38 0-2.5-1.12-2.5-2.5S10.62 10 12 10s2.5 1.12 2.5 2.5S13.38 15 12 15m6 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1m3-8.5c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1"/></svg>`;
175
248
  function defineButton(client, tagName = "clearly-hub-button") {
176
- if (typeof customElements === "undefined" || customElements.get(tagName)) return;
249
+ if (typeof customElements === "undefined" || customElements.get(tagName))
250
+ return;
177
251
  class ClearlyHubButton extends HTMLElement {
178
252
  constructor() {
179
253
  super();
@@ -184,7 +258,14 @@ function defineButton(client, tagName = "clearly-hub-button") {
184
258
  this.button.addEventListener("click", () => this.run());
185
259
  }
186
260
  connectedCallback() {
187
- for (const name of ["action", "display", "label", "variant", "size", "data"]) {
261
+ for (const name of [
262
+ "action",
263
+ "display",
264
+ "label",
265
+ "variant",
266
+ "size",
267
+ "data"
268
+ ]) {
188
269
  if (Object.prototype.hasOwnProperty.call(this, name)) {
189
270
  const value = this[name];
190
271
  delete this[name];
@@ -264,9 +345,17 @@ function defineButton(client, tagName = "clearly-hub-button") {
264
345
  });
265
346
  } catch (error) {
266
347
  console.error(error);
267
- const result = { action, result: "FAILURE", error: error.message };
348
+ const result = {
349
+ action,
350
+ result: "FAILURE",
351
+ error: error.message
352
+ };
268
353
  this.dispatchEvent(
269
- new CustomEvent(ANSWER_EVENT, { detail: result, bubbles: true, composed: true })
354
+ new CustomEvent(ANSWER_EVENT, {
355
+ detail: result,
356
+ bubbles: true,
357
+ composed: true
358
+ })
270
359
  );
271
360
  } finally {
272
361
  this.button.disabled = false;
@@ -281,11 +370,16 @@ function defineButton(client, tagName = "clearly-hub-button") {
281
370
  var clearlyHub = new ClearlyHubClient();
282
371
  defineButton(clearlyHub);
283
372
  if (typeof window !== "undefined") {
284
- queueMicrotask(() => {
373
+ const deliver = () => {
285
374
  void clearlyHub.result().then((result) => {
286
- if (result) window.dispatchEvent(new CustomEvent(ANSWER_EVENT, { detail: result }));
375
+ if (result) clearlyHub.deliverRedirectResult(result);
287
376
  });
288
- });
377
+ };
378
+ if (document.readyState === "loading") {
379
+ document.addEventListener("DOMContentLoaded", deliver, { once: true });
380
+ } else {
381
+ queueMicrotask(deliver);
382
+ }
289
383
  }
290
384
  export {
291
385
  ANSWER_EVENT,
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { DetailedHTMLProps, HTMLAttributes } from 'react';
2
1
  import { ScreenAction, ScreenDisplay } from './index.js';
2
+ import { DetailedHTMLProps, HTMLAttributes } from 'react';
3
3
 
4
4
  interface ClearlyHubButtonAttributes {
5
5
  action?: ScreenAction | 'sign-in' | 'sign-out';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clearly-hub/browser",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Clearly.HUB in the browser: sign-in, hosted screens and the session, talking to @clearly-hub/node",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Future Insight Group BV",