@clearly-hub/browser 0.3.0 → 0.4.1
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 +44 -10
- package/dist/clearly-hub.js +2 -2
- package/dist/index.d.ts +27 -19
- package/dist/index.js +89 -26
- package/dist/react.d.ts +1 -1
- package/package.json +1 -1
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.
|
|
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).
|
|
@@ -30,7 +31,7 @@ const hub = clearlyHub({
|
|
|
30
31
|
m2mClientId: process.env.CLEARLY_HUB_M2M_CLIENT_ID,
|
|
31
32
|
m2mClientSecret: process.env.CLEARLY_HUB_M2M_CLIENT_SECRET,
|
|
32
33
|
appUrl: 'https://app.example.com',
|
|
33
|
-
secret: process.env.SESSION_SECRET, // 32+ characters
|
|
34
|
+
secret: process.env.SESSION_SECRET, // 32+ random characters
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
const app = express();
|
|
@@ -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) =>
|
|
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,34 @@ 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: send it only to resource URLs from `DATA_PICKER` (Clearly.HUB's
|
|
118
|
+
data proxy), and keep scripts you don't trust off pages that use it. `fetchAsUser` refuses other URLs.
|
|
119
|
+
|
|
97
120
|
## The Node package
|
|
98
121
|
|
|
99
122
|
| | |
|
|
100
123
|
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
-
| `hub.express()`, `hub.handler` | The routes: `/sign-in`, `/callback`, `/sign-out`, `/session`, `/open`, `/answer`, `/result`
|
|
124
|
+
| `hub.express()`, `hub.handler` | The routes: `/sign-in`, `/callback`, `/sign-out`, `/session`, `/open`, `/answer`, `/result`, `/token` |
|
|
102
125
|
| `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
126
|
| `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
127
|
| `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
|
|
128
|
+
| `hub.fetchAsUser(req, url)` | Fetch protected or paid data (from `DATA_PICKER`) on your server, with the person's key. Only URLs on Clearly.HUB's data proxy |
|
|
106
129
|
| `hub.getSubscription(hubId)`, `getSubscriptions()` | A hub's subscription to your app, or all of them |
|
|
107
130
|
| `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
131
|
| `hub.getUserToken(req)`, `getAppToken()` | The person's key and your app's key, for calls of your own |
|
|
@@ -121,18 +144,29 @@ const hub = clearlyHub<express.Request>({
|
|
|
121
144
|
});
|
|
122
145
|
```
|
|
123
146
|
|
|
124
|
-
**What it checks for you:** every screen answer's Ed25519 signature,
|
|
125
|
-
opened in this browser, once (`state`)
|
|
126
|
-
|
|
147
|
+
**What it checks for you:** every screen answer's Ed25519 signature, that it answers a screen
|
|
148
|
+
opened in this browser, once (`state`), and that it was given to your app and that screen
|
|
149
|
+
(`client_id`, callback URL, action).
|
|
150
|
+
|
|
151
|
+
**What to trust in an answer:** it's a snapshot of the moment the person clicked. For access later,
|
|
152
|
+
ask `getSubscription(hubId)` when it's used, not the `status` you got then. Ignore `answer.user`:
|
|
153
|
+
Clearly.HUB doesn't check it yet. Who's signed in is the session's user.
|
|
154
|
+
|
|
155
|
+
The session is encrypted in cookies, so there's nothing to store; it's split over several cookies
|
|
156
|
+
because tokens are large. A copied cookie stays valid until its refresh token expires: signing out
|
|
157
|
+
clears only this browser's copy.
|
|
127
158
|
|
|
128
|
-
**Local Clearly.HUB:** pass `urls: { screens, api,
|
|
159
|
+
**Local Clearly.HUB:** pass `urls: { screens, api, proxy, auth, issuer }`; see
|
|
129
160
|
`examples/express/.env.example`.
|
|
130
161
|
|
|
131
162
|
## Not covered (yet)
|
|
132
163
|
|
|
133
164
|
- Setting up your own workspaces or tenants: do it by hand, with plans that need approval, or in
|
|
134
165
|
`onAnswer` for `SELECT_SUBSCRIPTION`.
|
|
135
|
-
- Other server languages:
|
|
166
|
+
- Other server languages (.NET, Python, ...): the browser package works with any
|
|
167
|
+
backend that answers the same routes. Contact us at
|
|
168
|
+
[support@futureinsight.nl](mailto:support@futureinsight.nl) and we'll help
|
|
169
|
+
you with yours.
|
|
136
170
|
|
|
137
171
|
## Working on this repo
|
|
138
172
|
|
package/dist/clearly-hub.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";(()=>{var C=Object.defineProperty;var T=(r,
|
|
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
|
-
`,
|
|
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
|
-
|
|
24
|
-
|
|
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), {
|
|
67
|
-
|
|
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)
|
|
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)
|
|
127
|
+
if (popup)
|
|
128
|
+
return this.waitForPopup(
|
|
129
|
+
popup,
|
|
130
|
+
ref,
|
|
131
|
+
action,
|
|
132
|
+
options.eventTarget ?? window
|
|
133
|
+
);
|
|
97
134
|
}
|
|
98
|
-
window.location.assign(
|
|
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)
|
|
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), {
|
|
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, {
|
|
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)
|
|
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))
|
|
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 [
|
|
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 = {
|
|
348
|
+
const result = {
|
|
349
|
+
action,
|
|
350
|
+
result: "FAILURE",
|
|
351
|
+
error: error.message
|
|
352
|
+
};
|
|
294
353
|
this.dispatchEvent(
|
|
295
|
-
new CustomEvent(ANSWER_EVENT, {
|
|
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
|
+
"version": "0.4.1",
|
|
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",
|