@clearly-hub/browser 0.3.0 → 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
 
@@ -65,6 +68,7 @@ Or `npm install @clearly-hub/browser` and `import { clearlyHub } from '@clearly-
65
68
  | `clearlyHub.signIn()`, `signOut()` | Go through Clearly.HUB and come back to this page |
66
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 |
67
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) |
68
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 |
69
73
  | `<clearly-hub-button action display data label>` | Branded button for `sign-in`, `sign-out` and each screen |
70
74
  | `clearly-hub-answer` event | Every answer, from a popup or after a redirect |
@@ -94,15 +98,33 @@ blocked popup falls back to a redirect.
94
98
  | `REGISTER_RESOURCE` | `{ url, format }` of your result, at a public https URL | `activeHub`, `dataset`, `resource` |
95
99
  | `SELECT_HUB` | | `hub` (`_id`, `name`) |
96
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
+
97
119
  ## The Node package
98
120
 
99
121
  | | |
100
122
  | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
101
- | `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` |
102
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 |
103
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` |
104
126
  | `hub.getUser(req)` | Who's signed in, in your own routes |
105
- | `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 |
106
128
  | `hub.getSubscription(hubId)`, `getSubscriptions()` | A hub's subscription to your app, or all of them |
107
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 |
108
130
  | `hub.getUserToken(req)`, `getAppToken()` | The person's key and your app's key, for calls of your own |
@@ -132,7 +154,10 @@ store; it's split over several cookies because tokens are large.
132
154
 
133
155
  - Setting up your own workspaces or tenants: do it by hand, with plans that need approval, or in
134
156
  `onAnswer` for `SELECT_SUBSCRIPTION`.
135
- - 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.
136
161
 
137
162
  ## Working on this repo
138
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 a=(r,t,n)=>T(r,typeof t!="symbol"?t+"":t,n);var x=["SELECT_SUBSCRIPTION","DATA_PICKER","REGISTER_RESOURCE","SELECT_HUB"];function E(r){return x.includes(r)}function S(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 R="/clearly-hub";var c={signIn:"/sign-in",callback:"/callback",signOut:"/sign-out",session:"/session",open:"/open",answer:"/answer",result:"/result"},b="clearly-hub-answers";var u="clearly-hub-answer",y=480,h=class{constructor(t={}){a(this,"basePath",R);a(this,"display","popup");a(this,"redirectResult");a(this,"unclaimedRedirectResult");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=S(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 l=window.open(this.url(c.open,{...s,display:"popup"}),"clearly-hub-screen",`width=${y},height=${screen.availHeight},left=${screen.availWidth-y}`);if(l)return this.waitForPopup(l,o,t,n.eventTarget??window)}return window.location.assign(this.url(c.open,{...s,display:"redirect"})),new Promise(()=>{})}onAnswer(t){let n=o=>{let e=o.detail;e===this.unclaimedRedirectResult&&(this.unclaimedRedirectResult=void 0),t(e)};window.addEventListener(u,n);let i=this.unclaimedRedirectResult;return i&&(this.unclaimedRedirectResult=void 0,t(i)),()=>window.removeEventListener(u,n)}deliverRedirectResult(t){this.unclaimedRedirectResult=t,window.dispatchEvent(new CustomEvent(u,{detail:t}))}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(b),l=!1,f=p=>{l||(l=!0,s.close(),window.clearInterval(g),o.dispatchEvent(new CustomEvent(u,{detail:p,bubbles:!0,composed:!0})),e(p))};s.onmessage=p=>{p.data?.ref===n&&f(p.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 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"},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 A(r,t="clearly-hub-button"){if(typeof customElements>"u"||customElements.get(t))return;class n extends HTMLElement{constructor(){super();a(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 w?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??w[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(E(e)){this.button.disabled=!0;try{await r.open(e,{display:this.display,data:this.data,eventTarget:this})}catch(s){console.error(s);let l={action:e,result:"FAILURE",error:s.message};this.dispatchEvent(new CustomEvent(u,{detail:l,bubbles:!0,composed:!0}))}finally{this.button.disabled=!1}}}}a(n,"observedAttributes",["action","label"]),customElements.define(t,n)}var d=new h;A(d);if(typeof window<"u"){let r=()=>{d.result().then(t=>{t&&d.deliverRedirectResult(t)})};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;})();
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 {
@@ -55,9 +60,12 @@ declare class ClearlyHubClient {
55
60
  private display;
56
61
  private redirectResult?;
57
62
  private unclaimedRedirectResult?;
63
+ private token?;
64
+ private tokenRequest?;
58
65
  constructor(options?: ClientOptions);
59
66
  configure(options: ClientOptions): this;
60
67
  session(): Promise<Session>;
68
+ getToken(): Promise<string | null>;
61
69
  signIn({ returnTo }?: {
62
70
  returnTo?: string;
63
71
  }): void;
@@ -75,4 +83,4 @@ declare function defineButton(client: ClearlyHubClient, tagName?: string): void;
75
83
  declare const clearlyHub: ClearlyHubClient;
76
84
 
77
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 };
78
- 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;
@@ -54,6 +55,8 @@ var ClearlyHubClient = class {
54
55
  // An answer after a redirect that no onAnswer() callback has had yet: the
55
56
  // first callback to come gets it
56
57
  __publicField(this, "unclaimedRedirectResult");
58
+ __publicField(this, "token");
59
+ __publicField(this, "tokenRequest");
57
60
  this.configure(options);
58
61
  }
59
62
  configure(options) {
@@ -63,10 +66,37 @@ var ClearlyHubClient = class {
63
66
  }
64
67
  // Who's signed in. Which of your tenants they work for is your app's to know
65
68
  async session() {
66
- const response = await fetch(this.url(ROUTES.session), { credentials: "same-origin" });
67
- 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})`);
68
74
  return response.json();
69
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
+ }
70
100
  signIn({ returnTo = currentPath() } = {}) {
71
101
  window.location.assign(this.url(ROUTES.signIn, { returnTo }));
72
102
  }
@@ -78,7 +108,8 @@ var ClearlyHubClient = class {
78
108
  // back to a redirect.
79
109
  open(action, options = {}) {
80
110
  const dataProblem = checkScreenData(action, options.data);
81
- if (dataProblem) return Promise.reject(new Error(`Clearly.HUB: ${dataProblem}`));
111
+ if (dataProblem)
112
+ return Promise.reject(new Error(`Clearly.HUB: ${dataProblem}`));
82
113
  const ref = crypto.randomUUID();
83
114
  const display = options.display ?? this.display;
84
115
  const query = {
@@ -93,9 +124,17 @@ var ClearlyHubClient = class {
93
124
  "clearly-hub-screen",
94
125
  `width=${POPUP_WIDTH},height=${screen.availHeight},left=${screen.availWidth - POPUP_WIDTH}`
95
126
  );
96
- 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
+ );
97
134
  }
98
- window.location.assign(this.url(ROUTES.open, { ...query, display: "redirect" }));
135
+ window.location.assign(
136
+ this.url(ROUTES.open, { ...query, display: "redirect" })
137
+ );
99
138
  return new Promise(() => {
100
139
  });
101
140
  }
@@ -105,7 +144,8 @@ var ClearlyHubClient = class {
105
144
  onAnswer(callback) {
106
145
  const listener = (event) => {
107
146
  const result = event.detail;
108
- if (result === this.unclaimedRedirectResult) this.unclaimedRedirectResult = void 0;
147
+ if (result === this.unclaimedRedirectResult)
148
+ this.unclaimedRedirectResult = void 0;
109
149
  callback(result);
110
150
  };
111
151
  window.addEventListener(ANSWER_EVENT, listener);
@@ -125,7 +165,9 @@ var ClearlyHubClient = class {
125
165
  // After a redirect screen: its answer, once, on the page it came back to.
126
166
  // Null when there is none.
127
167
  result() {
128
- 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));
129
171
  return this.redirectResult;
130
172
  }
131
173
  waitForPopup(popup, ref, action, eventTarget) {
@@ -138,12 +180,17 @@ var ClearlyHubClient = class {
138
180
  channel.close();
139
181
  window.clearInterval(closedCheck);
140
182
  eventTarget.dispatchEvent(
141
- 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
+ })
142
188
  );
143
189
  resolve(result);
144
190
  };
145
191
  channel.onmessage = (event) => {
146
- if (event.data?.ref === ref) finish(event.data.result);
192
+ if (event.data?.ref === ref)
193
+ finish(event.data.result);
147
194
  };
148
195
  const closedCheck = window.setInterval(() => {
149
196
  if (popup.closed) {
@@ -199,7 +246,8 @@ var STYLE = `
199
246
  `;
200
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>`;
201
248
  function defineButton(client, tagName = "clearly-hub-button") {
202
- if (typeof customElements === "undefined" || customElements.get(tagName)) return;
249
+ if (typeof customElements === "undefined" || customElements.get(tagName))
250
+ return;
203
251
  class ClearlyHubButton extends HTMLElement {
204
252
  constructor() {
205
253
  super();
@@ -210,7 +258,14 @@ function defineButton(client, tagName = "clearly-hub-button") {
210
258
  this.button.addEventListener("click", () => this.run());
211
259
  }
212
260
  connectedCallback() {
213
- 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
+ ]) {
214
269
  if (Object.prototype.hasOwnProperty.call(this, name)) {
215
270
  const value = this[name];
216
271
  delete this[name];
@@ -290,9 +345,17 @@ function defineButton(client, tagName = "clearly-hub-button") {
290
345
  });
291
346
  } catch (error) {
292
347
  console.error(error);
293
- const result = { action, result: "FAILURE", error: error.message };
348
+ const result = {
349
+ action,
350
+ result: "FAILURE",
351
+ error: error.message
352
+ };
294
353
  this.dispatchEvent(
295
- 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
+ })
296
359
  );
297
360
  } finally {
298
361
  this.button.disabled = false;
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.3.0",
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",