@clearly-hub/browser 0.0.0-stage → 0.1.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/LICENSE +14 -0
- package/README.md +125 -2
- package/dist/clearly-hub.js +23 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.js +220 -0
- package/package.json +34 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Copyright (c) 2026 Future Insight Group BV. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software is proprietary. You may install and use it, unmodified, only to
|
|
4
|
+
connect your own applications to Clearly.HUB. You may not copy, modify,
|
|
5
|
+
publish, distribute, sublicense or sell it, or parts of it, for any other
|
|
6
|
+
purpose without prior written permission from Future Insight Group BV.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
9
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
10
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
11
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
12
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
13
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
14
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,126 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Clearly.HUB SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Connect your web app to Clearly.HUB with a script, some config and one server handler. You get
|
|
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.
|
|
6
|
+
|
|
7
|
+
> **Preview (0.x).** The API may still change between minor versions. Proprietary: see
|
|
8
|
+
> [LICENSE](LICENSE).
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
**1. Register your app in Clearly.HUB** (on your app's page, no code):
|
|
13
|
+
|
|
14
|
+
| Setting | Value |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| Go to app | `https://app.example.com/clearly-hub/sign-in` |
|
|
17
|
+
| Sign-in client (H2M): callback URLs | `https://app.example.com/clearly-hub/callback`, `https://app.example.com/clearly-hub/answer`, `https://hub.clearly.app/components` |
|
|
18
|
+
| Sign-in client: sign-out URLs | `https://app.example.com`, `https://hub.clearly.app/components` |
|
|
19
|
+
| M2M client | create it, keep the secret on your server |
|
|
20
|
+
| Plans | give each your own plan id as its *External reference*, for example `pro` |
|
|
21
|
+
|
|
22
|
+
**2. Mount the server handler** (Node 20+):
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import express from 'express';
|
|
26
|
+
import { clearlyHub } from '@clearly-hub/node';
|
|
27
|
+
|
|
28
|
+
const hub = clearlyHub({
|
|
29
|
+
clientId: process.env.CLEARLY_HUB_CLIENT_ID,
|
|
30
|
+
m2mClientId: process.env.CLEARLY_HUB_M2M_CLIENT_ID,
|
|
31
|
+
m2mClientSecret: process.env.CLEARLY_HUB_M2M_CLIENT_SECRET,
|
|
32
|
+
appUrl: 'https://app.example.com',
|
|
33
|
+
secret: process.env.SESSION_SECRET, // 32+ characters
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const app = express();
|
|
37
|
+
app.use('/clearly-hub', hub.express());
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Not Express? `hub.handler` takes a `Request` and returns a `Response`, for Next.js route handlers,
|
|
41
|
+
Hono, Bun or Deno. Mount it at `/clearly-hub/*`.
|
|
42
|
+
|
|
43
|
+
**3. Add the script and a button:**
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<script src="https://cdn.jsdelivr.net/npm/@clearly-hub/browser/dist/clearly-hub.js"></script>
|
|
47
|
+
|
|
48
|
+
<clearly-hub-button action="sign-in"></clearly-hub-button>
|
|
49
|
+
<clearly-hub-button action="DATA_PICKER"></clearly-hub-button>
|
|
50
|
+
|
|
51
|
+
<script type="module">
|
|
52
|
+
const session = await clearlyHub.session(); // { signedIn, user }
|
|
53
|
+
window.addEventListener('clearly-hub-answer', (event) => console.log(event.detail));
|
|
54
|
+
</script>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or `npm install @clearly-hub/browser` and `import { clearlyHub } from '@clearly-hub/browser'`.
|
|
58
|
+
|
|
59
|
+
## The browser package
|
|
60
|
+
|
|
61
|
+
| | |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `clearlyHub.session()` | `{ signedIn: false }`, or the signed-in user (`sub`, `email`, `name`, `appUserId`) |
|
|
64
|
+
| `clearlyHub.signIn()`, `signOut()` | Go through Clearly.HUB and come back to this page |
|
|
65
|
+
| `clearlyHub.open(action, { data, display })` | Opens a hosted screen; resolves with its checked answer. Call it from a click: browsers only allow popups there |
|
|
66
|
+
| `clearlyHub.result()` | After a redirect screen: its answer, on the page it came back to |
|
|
67
|
+
| `<clearly-hub-button action display data label>` | Branded button for `sign-in`, `sign-out` and each screen |
|
|
68
|
+
| `clearly-hub-answer` event | Every answer, from a popup or after a redirect |
|
|
69
|
+
|
|
70
|
+
**Styling the button.** It uses your page's font. Attributes for the common cases:
|
|
71
|
+
`variant="outline"`, `size="small"` or `size="large"`. CSS custom properties for your look:
|
|
72
|
+
`--clearly-hub-color`, `--clearly-hub-text-color`, `--clearly-hub-radius`, `--clearly-hub-padding`,
|
|
73
|
+
`--clearly-hub-font`. For anything else, style the real button through `::part(button)`:
|
|
74
|
+
|
|
75
|
+
```css
|
|
76
|
+
clearly-hub-button { --clearly-hub-color: #0b7471; --clearly-hub-radius: 999px; }
|
|
77
|
+
clearly-hub-button::part(button) { text-transform: uppercase; }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Screens open in a popup by default (`data-display="redirect"` on the script tag changes that). A
|
|
81
|
+
blocked popup falls back to a redirect.
|
|
82
|
+
|
|
83
|
+
| Action | Send in `data` | `answer` when `result` is `SUCCESS` |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `SELECT_SUBSCRIPTION` | | `subscription` (`_id`, `status`, `externalReference`), `subscribedHub` (`_id`, `name`, `role`) |
|
|
86
|
+
| `DATA_PICKER` | | `activeHub`, `resources` (`url`, `format`, `accessibility`, `requiresToken`, `hasAccess`) |
|
|
87
|
+
| `REGISTER_RESOURCE` | `{ url, format }` of your result, at a public https URL | `activeHub`, `dataset`, `resource` |
|
|
88
|
+
| `SELECT_HUB` | | `hub` (`_id`, `name`) |
|
|
89
|
+
|
|
90
|
+
## The Node package
|
|
91
|
+
|
|
92
|
+
| | |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `hub.express()`, `hub.handler` | The routes: `/sign-in`, `/callback`, `/sign-out`, `/session`, `/open`, `/answer`, `/result` |
|
|
95
|
+
| `onSignIn(person)` | Called after each sign-in; return your own user id to keep it in the session |
|
|
96
|
+
| `onAnswer(result, { user })` | Called with each checked answer before it reaches the browser, for example to set up a workspace |
|
|
97
|
+
| `hub.getUser(req)` | Who's signed in, in your own routes |
|
|
98
|
+
| `hub.fetchAsUser(req, url)` | Fetch protected or paid data (from `DATA_PICKER`) with the person's key |
|
|
99
|
+
| `hub.getSubscription(hubId)`, `getSubscriptions()` | A hub's subscription to your app, or all of them |
|
|
100
|
+
| `hub.reportUsage({ request, hubId, quantity })` | Report billable use; Clearly.HUB bills the hub |
|
|
101
|
+
| `hub.getUserToken(req)`, `getAppToken()` | The person's key and your app's key, for calls of your own |
|
|
102
|
+
|
|
103
|
+
**What it checks for you:** every screen answer's Ed25519 signature, and that it answers a screen
|
|
104
|
+
opened in this browser, once (`state`). The session is encrypted in cookies, so there's nothing to
|
|
105
|
+
store; it's split over several cookies because tokens are large.
|
|
106
|
+
|
|
107
|
+
**Local Clearly.HUB:** pass `urls: { screens, api, usage, auth, issuer }`; see
|
|
108
|
+
`examples/express/.env.example`.
|
|
109
|
+
|
|
110
|
+
## Not covered (yet)
|
|
111
|
+
|
|
112
|
+
- Setting up your own workspaces or tenants: do it by hand, with plans that need approval, or in
|
|
113
|
+
`onAnswer` for `SELECT_SUBSCRIPTION`.
|
|
114
|
+
- Child hubs in `session().hubs`: Clearly.HUB only returns root hubs today.
|
|
115
|
+
- Other server languages: follow the Build checklist.
|
|
116
|
+
|
|
117
|
+
## Working on this repo
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
npm install
|
|
121
|
+
npm run build # both packages
|
|
122
|
+
npm test # vitest
|
|
123
|
+
npm run typecheck
|
|
124
|
+
npm run lint
|
|
125
|
+
npm run example # examples/express on http://localhost:5175 (copy .env.example to .env first)
|
|
126
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";(()=>{var R=Object.defineProperty;var C=(r,e,n)=>e in r?R(r,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):r[e]=n;var a=(r,e,n)=>C(r,typeof e!="symbol"?e+"":e,n);var x=["SELECT_SUBSCRIPTION","DATA_PICKER","REGISTER_RESOURCE","SELECT_HUB"];function f(r){return x.includes(r)}var S="/clearly-hub";var c={signIn:"/sign-in",callback:"/callback",signOut:"/sign-out",session:"/session",open:"/open",answer:"/answer",result:"/result"},g="clearly-hub-answers";var u="clearly-hub-answer",E=480,b=class{constructor(e={}){a(this,"basePath",S);a(this,"display","popup");a(this,"redirectResult");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(c.session),{credentials:"same-origin"});if(!e.ok)throw new Error(`Clearly.HUB session failed (${e.status})`);return e.json()}signIn({returnTo:e=y()}={}){window.location.assign(this.url(c.signIn,{returnTo:e}))}signOut(){window.location.assign(this.url(c.signOut))}open(e,n={}){let i=crypto.randomUUID(),o=n.display??this.display,t={action:e,ref:i,returnTo:n.returnTo??y(),data:n.data?JSON.stringify(n.data):void 0};if(o==="popup"){let s=window.open(this.url(c.open,{...t,display:"popup"}),"clearly-hub-screen",`width=${E},height=${screen.availHeight},left=${screen.availWidth-E}`);if(s)return this.waitForPopup(s,i,e)}return window.location.assign(this.url(c.open,{...t,display:"redirect"})),new Promise(()=>{})}result(){return this.redirectResult??(this.redirectResult=fetch(this.url(c.result),{credentials:"same-origin"}).then(e=>e.ok?e.json():null).then(e=>e?.result??null).catch(()=>null)),this.redirectResult}waitForPopup(e,n,i){return new Promise(o=>{let t=new BroadcastChannel(g),s=!1,d=l=>{s||(s=!0,t.close(),window.clearInterval(h),window.dispatchEvent(new CustomEvent(u,{detail:l})),o(l))};t.onmessage=l=>{l.data?.ref===n&&d(l.data.result)};let h=window.setInterval(()=>{e.closed&&(window.clearInterval(h),window.setTimeout(()=>d({action:i,result:"CANCELLED"}),500))},400)})}url(e,n={}){let i=new URLSearchParams;for(let[t,s]of Object.entries(n))s!==void 0&&i.set(t,s);let o=i.toString();return`${this.basePath}${e}${o?`?${o}`:""}`}};function y(){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"},v=`
|
|
2
|
+
:host { display: inline-block; }
|
|
3
|
+
button {
|
|
4
|
+
--color: var(--clearly-hub-color, #1fa69d);
|
|
5
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
6
|
+
padding: var(--clearly-hub-padding, 8px 16px);
|
|
7
|
+
border: 1px solid var(--color);
|
|
8
|
+
border-radius: var(--clearly-hub-radius, 6px);
|
|
9
|
+
background: var(--color);
|
|
10
|
+
color: var(--clearly-hub-text-color, #fff);
|
|
11
|
+
font: var(--clearly-hub-font, inherit);
|
|
12
|
+
font-weight: 600;
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
}
|
|
15
|
+
:host([variant="outline"]) button { background: transparent; color: var(--color); }
|
|
16
|
+
:host([size="small"]) button { padding: var(--clearly-hub-padding, 4px 10px); font-size: 0.875em; }
|
|
17
|
+
:host([size="large"]) button { padding: var(--clearly-hub-padding, 12px 22px); font-size: 1.125em; }
|
|
18
|
+
button:hover { filter: brightness(0.93); }
|
|
19
|
+
:host([variant="outline"]) button:hover { filter: none; background: color-mix(in srgb, var(--color) 10%, transparent); }
|
|
20
|
+
button:focus-visible { outline: 2px solid var(--color); outline-offset: 2px; }
|
|
21
|
+
button:disabled { opacity: 0.6; cursor: default; }
|
|
22
|
+
svg { width: 1.25em; height: 1.25em; flex: none; fill: currentColor; }
|
|
23
|
+
`,T='<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 m(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>${v}</style><button part="button" type="button"></button>`,this.button=t.querySelector("button"),this.button.addEventListener("click",()=>this.run())}connectedCallback(){this.render()}attributeChangedCallback(){this.render()}get action(){let t=this.getAttribute("action")??"sign-in";return t in A?t:"sign-in"}render(){let t=this.getAttribute("label")??A[this.action];this.button.innerHTML=`${T}<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(!f(t))return;let s=this.getAttribute("data"),d=this.getAttribute("display")==="redirect"?"redirect":void 0;this.button.disabled=!0;try{let h=await r.open(t,{display:d,data:s?JSON.parse(s):void 0});this.dispatchEvent(new CustomEvent(u,{detail:h,bubbles:!0,composed:!0}))}finally{this.button.disabled=!1}}}a(n,"observedAttributes",["action","label"]),customElements.define(e,n)}var p=new b;m(p);typeof window<"u"&&queueMicrotask(()=>{p.result().then(r=>{r&&window.dispatchEvent(new CustomEvent(u,{detail:r}))})});var w=document.currentScript;p.configure({basePath:w?.dataset.basePath,display:w?.dataset.display==="redirect"?"redirect":void 0});window.clearlyHub=p;})();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
declare const SCREEN_ACTIONS: readonly ["SELECT_SUBSCRIPTION", "DATA_PICKER", "REGISTER_RESOURCE", "SELECT_HUB"];
|
|
2
|
+
type ScreenAction = (typeof SCREEN_ACTIONS)[number];
|
|
3
|
+
declare function isScreenAction(value: unknown): value is ScreenAction;
|
|
4
|
+
type ScreenDisplay = 'redirect' | 'popup';
|
|
5
|
+
interface ScreenData {
|
|
6
|
+
REGISTER_RESOURCE: {
|
|
7
|
+
url: string;
|
|
8
|
+
format: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
interface HubRef {
|
|
12
|
+
_id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
interface PickedResource {
|
|
16
|
+
datasetTitle: string;
|
|
17
|
+
resourceName: string;
|
|
18
|
+
format: string;
|
|
19
|
+
accessibility: 'OPEN' | 'PROTECTED' | 'PAID';
|
|
20
|
+
url: string;
|
|
21
|
+
requiresToken: boolean;
|
|
22
|
+
hasAccess: boolean;
|
|
23
|
+
}
|
|
24
|
+
interface ScreenAnswers {
|
|
25
|
+
SELECT_SUBSCRIPTION: {
|
|
26
|
+
subscription: {
|
|
27
|
+
_id: string;
|
|
28
|
+
status: 'ACTIVE' | 'PENDING';
|
|
29
|
+
externalReference: string;
|
|
30
|
+
};
|
|
31
|
+
subscribedHub: HubRef & {
|
|
32
|
+
role?: 'ADMIN' | 'MEMBER';
|
|
33
|
+
};
|
|
34
|
+
user?: {
|
|
35
|
+
_id: string;
|
|
36
|
+
email: string;
|
|
37
|
+
};
|
|
38
|
+
hasMultipleSubscriptions?: boolean;
|
|
39
|
+
};
|
|
40
|
+
DATA_PICKER: {
|
|
41
|
+
activeHub: HubRef;
|
|
42
|
+
resources: PickedResource[];
|
|
43
|
+
};
|
|
44
|
+
REGISTER_RESOURCE: {
|
|
45
|
+
activeHub: string;
|
|
46
|
+
dataset: {
|
|
47
|
+
datasetId: string;
|
|
48
|
+
datasetTitle?: string;
|
|
49
|
+
};
|
|
50
|
+
resource: {
|
|
51
|
+
resourceId: string;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
SELECT_HUB: {
|
|
55
|
+
hub: HubRef;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
type ScreenResult<A extends ScreenAction = ScreenAction> = {
|
|
59
|
+
action: A;
|
|
60
|
+
result: 'SUCCESS';
|
|
61
|
+
answer: ScreenAnswers[A];
|
|
62
|
+
} | {
|
|
63
|
+
action: A;
|
|
64
|
+
result: 'CANCELLED' | 'FAILURE';
|
|
65
|
+
answer?: undefined;
|
|
66
|
+
error?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
interface SessionUser {
|
|
70
|
+
sub: string;
|
|
71
|
+
email: string;
|
|
72
|
+
name?: string;
|
|
73
|
+
appUserId?: string;
|
|
74
|
+
}
|
|
75
|
+
interface HubSubscription {
|
|
76
|
+
subscriptionId: string;
|
|
77
|
+
status: 'ACTIVE' | 'PENDING';
|
|
78
|
+
planName: string;
|
|
79
|
+
planId?: string;
|
|
80
|
+
}
|
|
81
|
+
type Session = {
|
|
82
|
+
signedIn: false;
|
|
83
|
+
} | {
|
|
84
|
+
signedIn: true;
|
|
85
|
+
user: SessionUser;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
declare const DEFAULT_BASE_PATH = "/clearly-hub";
|
|
89
|
+
declare const DEFAULT_HUB_URL = "https://hub.clearly.app";
|
|
90
|
+
declare const DEFAULT_AUTH_URL = "https://auth.clearly.app";
|
|
91
|
+
declare const DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
|
|
92
|
+
declare const ROUTES: {
|
|
93
|
+
readonly signIn: "/sign-in";
|
|
94
|
+
readonly callback: "/callback";
|
|
95
|
+
readonly signOut: "/sign-out";
|
|
96
|
+
readonly session: "/session";
|
|
97
|
+
readonly open: "/open";
|
|
98
|
+
readonly answer: "/answer";
|
|
99
|
+
readonly result: "/result";
|
|
100
|
+
};
|
|
101
|
+
declare const ANSWER_CHANNEL = "clearly-hub-answers";
|
|
102
|
+
interface AnswerMessage {
|
|
103
|
+
ref: string;
|
|
104
|
+
result: ScreenResult;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface ClientOptions {
|
|
108
|
+
basePath?: string;
|
|
109
|
+
display?: ScreenDisplay;
|
|
110
|
+
}
|
|
111
|
+
interface OpenOptions<A extends ScreenAction> {
|
|
112
|
+
display?: ScreenDisplay;
|
|
113
|
+
data?: A extends keyof ScreenData ? ScreenData[A] : never;
|
|
114
|
+
returnTo?: string;
|
|
115
|
+
}
|
|
116
|
+
declare const ANSWER_EVENT = "clearly-hub-answer";
|
|
117
|
+
declare class ClearlyHubClient {
|
|
118
|
+
private basePath;
|
|
119
|
+
private display;
|
|
120
|
+
private redirectResult?;
|
|
121
|
+
constructor(options?: ClientOptions);
|
|
122
|
+
configure(options: ClientOptions): this;
|
|
123
|
+
session(): Promise<Session>;
|
|
124
|
+
signIn({ returnTo }?: {
|
|
125
|
+
returnTo?: string;
|
|
126
|
+
}): void;
|
|
127
|
+
signOut(): void;
|
|
128
|
+
open<A extends ScreenAction>(action: A, options?: OpenOptions<A>): Promise<ScreenResult<A>>;
|
|
129
|
+
result(): Promise<ScreenResult | null>;
|
|
130
|
+
private waitForPopup;
|
|
131
|
+
private url;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
declare function defineButton(client: ClearlyHubClient, tagName?: string): void;
|
|
135
|
+
|
|
136
|
+
declare const clearlyHub: ClearlyHubClient;
|
|
137
|
+
|
|
138
|
+
export { ANSWER_CHANNEL, ANSWER_EVENT, ClearlyHubClient, DEFAULT_AUTH_URL, DEFAULT_BASE_PATH, DEFAULT_HUB_URL, DEFAULT_ISSUER, ROUTES, SCREEN_ACTIONS, clearlyHub, defineButton, isScreenAction };
|
|
139
|
+
export type { AnswerMessage, ClientOptions, HubRef, HubSubscription, OpenOptions, PickedResource, ScreenAction, ScreenAnswers, ScreenData, ScreenDisplay, ScreenResult, Session, SessionUser };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// ../shared/src/screens.ts
|
|
6
|
+
var SCREEN_ACTIONS = [
|
|
7
|
+
"SELECT_SUBSCRIPTION",
|
|
8
|
+
"DATA_PICKER",
|
|
9
|
+
"REGISTER_RESOURCE",
|
|
10
|
+
"SELECT_HUB"
|
|
11
|
+
];
|
|
12
|
+
function isScreenAction(value) {
|
|
13
|
+
return SCREEN_ACTIONS.includes(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ../shared/src/protocol.ts
|
|
17
|
+
var DEFAULT_BASE_PATH = "/clearly-hub";
|
|
18
|
+
var ROUTES = {
|
|
19
|
+
signIn: "/sign-in",
|
|
20
|
+
callback: "/callback",
|
|
21
|
+
signOut: "/sign-out",
|
|
22
|
+
session: "/session",
|
|
23
|
+
open: "/open",
|
|
24
|
+
answer: "/answer",
|
|
25
|
+
result: "/result"
|
|
26
|
+
};
|
|
27
|
+
var ANSWER_CHANNEL = "clearly-hub-answers";
|
|
28
|
+
|
|
29
|
+
// src/client.ts
|
|
30
|
+
var ANSWER_EVENT = "clearly-hub-answer";
|
|
31
|
+
var POPUP_WIDTH = 480;
|
|
32
|
+
var ClearlyHubClient = class {
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
__publicField(this, "basePath", DEFAULT_BASE_PATH);
|
|
35
|
+
__publicField(this, "display", "popup");
|
|
36
|
+
__publicField(this, "redirectResult");
|
|
37
|
+
this.configure(options);
|
|
38
|
+
}
|
|
39
|
+
configure(options) {
|
|
40
|
+
if (options.basePath) this.basePath = options.basePath.replace(/\/+$/, "");
|
|
41
|
+
if (options.display) this.display = options.display;
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
// Who's signed in. Which of your tenants they work for is your app's to know
|
|
45
|
+
async session() {
|
|
46
|
+
const response = await fetch(this.url(ROUTES.session), { credentials: "same-origin" });
|
|
47
|
+
if (!response.ok) throw new Error(`Clearly.HUB session failed (${response.status})`);
|
|
48
|
+
return response.json();
|
|
49
|
+
}
|
|
50
|
+
signIn({ returnTo = currentPath() } = {}) {
|
|
51
|
+
window.location.assign(this.url(ROUTES.signIn, { returnTo }));
|
|
52
|
+
}
|
|
53
|
+
signOut() {
|
|
54
|
+
window.location.assign(this.url(ROUTES.signOut));
|
|
55
|
+
}
|
|
56
|
+
// Opens a hosted screen and resolves with its checked answer. Call it from a
|
|
57
|
+
// click handler: browsers only allow popups there. A blocked popup falls
|
|
58
|
+
// back to a redirect.
|
|
59
|
+
open(action, options = {}) {
|
|
60
|
+
const ref = crypto.randomUUID();
|
|
61
|
+
const display = options.display ?? this.display;
|
|
62
|
+
const query = {
|
|
63
|
+
action,
|
|
64
|
+
ref,
|
|
65
|
+
returnTo: options.returnTo ?? currentPath(),
|
|
66
|
+
data: options.data ? JSON.stringify(options.data) : void 0
|
|
67
|
+
};
|
|
68
|
+
if (display === "popup") {
|
|
69
|
+
const popup = window.open(
|
|
70
|
+
this.url(ROUTES.open, { ...query, display: "popup" }),
|
|
71
|
+
"clearly-hub-screen",
|
|
72
|
+
`width=${POPUP_WIDTH},height=${screen.availHeight},left=${screen.availWidth - POPUP_WIDTH}`
|
|
73
|
+
);
|
|
74
|
+
if (popup) return this.waitForPopup(popup, ref, action);
|
|
75
|
+
}
|
|
76
|
+
window.location.assign(this.url(ROUTES.open, { ...query, display: "redirect" }));
|
|
77
|
+
return new Promise(() => {
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// After a redirect screen: its answer, once, on the page it came back to.
|
|
81
|
+
// Null when there is none.
|
|
82
|
+
result() {
|
|
83
|
+
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));
|
|
84
|
+
return this.redirectResult;
|
|
85
|
+
}
|
|
86
|
+
waitForPopup(popup, ref, action) {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
const channel = new BroadcastChannel(ANSWER_CHANNEL);
|
|
89
|
+
let isDone = false;
|
|
90
|
+
const finish = (result) => {
|
|
91
|
+
if (isDone) return;
|
|
92
|
+
isDone = true;
|
|
93
|
+
channel.close();
|
|
94
|
+
window.clearInterval(closedCheck);
|
|
95
|
+
window.dispatchEvent(new CustomEvent(ANSWER_EVENT, { detail: result }));
|
|
96
|
+
resolve(result);
|
|
97
|
+
};
|
|
98
|
+
channel.onmessage = (event) => {
|
|
99
|
+
if (event.data?.ref === ref) finish(event.data.result);
|
|
100
|
+
};
|
|
101
|
+
const closedCheck = window.setInterval(() => {
|
|
102
|
+
if (popup.closed) {
|
|
103
|
+
window.clearInterval(closedCheck);
|
|
104
|
+
window.setTimeout(() => finish({ action, result: "CANCELLED" }), 500);
|
|
105
|
+
}
|
|
106
|
+
}, 400);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
url(route, query = {}) {
|
|
110
|
+
const params = new URLSearchParams();
|
|
111
|
+
for (const [key, value] of Object.entries(query))
|
|
112
|
+
if (value !== void 0) params.set(key, value);
|
|
113
|
+
const search = params.toString();
|
|
114
|
+
return `${this.basePath}${route}${search ? `?${search}` : ""}`;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
function currentPath() {
|
|
118
|
+
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/button.ts
|
|
122
|
+
var LABELS = {
|
|
123
|
+
"sign-in": "Sign in with Clearly.HUB",
|
|
124
|
+
"sign-out": "Sign out",
|
|
125
|
+
SELECT_SUBSCRIPTION: "Choose a plan in Clearly.HUB",
|
|
126
|
+
DATA_PICKER: "Pick data from Clearly.HUB",
|
|
127
|
+
REGISTER_RESOURCE: "Save to Clearly.HUB",
|
|
128
|
+
SELECT_HUB: "Choose a hub"
|
|
129
|
+
};
|
|
130
|
+
var STYLE = `
|
|
131
|
+
:host { display: inline-block; }
|
|
132
|
+
button {
|
|
133
|
+
--color: var(--clearly-hub-color, #1fa69d);
|
|
134
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
135
|
+
padding: var(--clearly-hub-padding, 8px 16px);
|
|
136
|
+
border: 1px solid var(--color);
|
|
137
|
+
border-radius: var(--clearly-hub-radius, 6px);
|
|
138
|
+
background: var(--color);
|
|
139
|
+
color: var(--clearly-hub-text-color, #fff);
|
|
140
|
+
font: var(--clearly-hub-font, inherit);
|
|
141
|
+
font-weight: 600;
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
}
|
|
144
|
+
:host([variant="outline"]) button { background: transparent; color: var(--color); }
|
|
145
|
+
:host([size="small"]) button { padding: var(--clearly-hub-padding, 4px 10px); font-size: 0.875em; }
|
|
146
|
+
:host([size="large"]) button { padding: var(--clearly-hub-padding, 12px 22px); font-size: 1.125em; }
|
|
147
|
+
button:hover { filter: brightness(0.93); }
|
|
148
|
+
:host([variant="outline"]) button:hover { filter: none; background: color-mix(in srgb, var(--color) 10%, transparent); }
|
|
149
|
+
button:focus-visible { outline: 2px solid var(--color); outline-offset: 2px; }
|
|
150
|
+
button:disabled { opacity: 0.6; cursor: default; }
|
|
151
|
+
svg { width: 1.25em; height: 1.25em; flex: none; fill: currentColor; }
|
|
152
|
+
`;
|
|
153
|
+
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>`;
|
|
154
|
+
function defineButton(client, tagName = "clearly-hub-button") {
|
|
155
|
+
if (typeof customElements === "undefined" || customElements.get(tagName)) return;
|
|
156
|
+
class ClearlyHubButton extends HTMLElement {
|
|
157
|
+
constructor() {
|
|
158
|
+
super();
|
|
159
|
+
__publicField(this, "button");
|
|
160
|
+
const root = this.attachShadow({ mode: "open" });
|
|
161
|
+
root.innerHTML = `<style>${STYLE}</style><button part="button" type="button"></button>`;
|
|
162
|
+
this.button = root.querySelector("button");
|
|
163
|
+
this.button.addEventListener("click", () => this.run());
|
|
164
|
+
}
|
|
165
|
+
connectedCallback() {
|
|
166
|
+
this.render();
|
|
167
|
+
}
|
|
168
|
+
attributeChangedCallback() {
|
|
169
|
+
this.render();
|
|
170
|
+
}
|
|
171
|
+
get action() {
|
|
172
|
+
const action = this.getAttribute("action") ?? "sign-in";
|
|
173
|
+
return action in LABELS ? action : "sign-in";
|
|
174
|
+
}
|
|
175
|
+
render() {
|
|
176
|
+
const label = this.getAttribute("label") ?? LABELS[this.action];
|
|
177
|
+
this.button.innerHTML = `${ICON}<span></span>`;
|
|
178
|
+
this.button.querySelector("span").textContent = label;
|
|
179
|
+
}
|
|
180
|
+
async run() {
|
|
181
|
+
const action = this.action;
|
|
182
|
+
if (action === "sign-in") return client.signIn();
|
|
183
|
+
if (action === "sign-out") return client.signOut();
|
|
184
|
+
if (!isScreenAction(action)) return;
|
|
185
|
+
const data = this.getAttribute("data");
|
|
186
|
+
const display = this.getAttribute("display") === "redirect" ? "redirect" : void 0;
|
|
187
|
+
this.button.disabled = true;
|
|
188
|
+
try {
|
|
189
|
+
const result = await client.open(action, {
|
|
190
|
+
display,
|
|
191
|
+
data: data ? JSON.parse(data) : void 0
|
|
192
|
+
});
|
|
193
|
+
this.dispatchEvent(
|
|
194
|
+
new CustomEvent(ANSWER_EVENT, { detail: result, bubbles: true, composed: true })
|
|
195
|
+
);
|
|
196
|
+
} finally {
|
|
197
|
+
this.button.disabled = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
__publicField(ClearlyHubButton, "observedAttributes", ["action", "label"]);
|
|
202
|
+
customElements.define(tagName, ClearlyHubButton);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/index.ts
|
|
206
|
+
var clearlyHub = new ClearlyHubClient();
|
|
207
|
+
defineButton(clearlyHub);
|
|
208
|
+
if (typeof window !== "undefined") {
|
|
209
|
+
queueMicrotask(() => {
|
|
210
|
+
void clearlyHub.result().then((result) => {
|
|
211
|
+
if (result) window.dispatchEvent(new CustomEvent(ANSWER_EVENT, { detail: result }));
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
export {
|
|
216
|
+
ANSWER_EVENT,
|
|
217
|
+
ClearlyHubClient,
|
|
218
|
+
clearlyHub,
|
|
219
|
+
defineButton
|
|
220
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clearly-hub/browser",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Clearly.HUB in the browser: sign-in, hosted screens and the session, talking to @clearly-hub/node",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"author": "Future Insight Group BV",
|
|
7
|
+
"keywords": ["clearly-hub", "oup", "open-urban-platform", "web-component", "sdk"],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./clearly-hub.js": "./dist/clearly-hub.js"
|
|
17
|
+
},
|
|
18
|
+
"unpkg": "./dist/clearly-hub.js",
|
|
19
|
+
"jsdelivr": "./dist/clearly-hub.js",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": true,
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"typecheck": "tsc -p .",
|
|
31
|
+
"prepack": "node ../../scripts/copy-license.mjs && npm run build"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@clearly-hub/shared": "0.1.0"
|
|
35
|
+
}
|
|
36
|
+
}
|