@clearly-hub/browser

中文文档
0.12.0发布于 2026/10/30SEE LICENSE IN LICENSETypeScriptES Module零依赖近 30 天下载 3191

Clearly.HUB in the browser: sign-in, hosted screens and the session, talking to @clearly-hub/node

原始使用文档
文档目录8 节
  1. Quick start
  2. The browser package
  3. Protected data
  4. The Node package
  5. Behind the data proxy
  6. Registering data
  7. Not covered (yet)
  8. Working on this repo

Clearly.HUB SDK

Connect your web app to Clearly.HUB with a script, some config and one server handler. You get "Sign in with Clearly.HUB", the hosted screens (choose a plan, pick data, save a result, choose a hub) and each hub's subscription to your app. The refresh token stays on your server; the page gets the access token only when it asks, for protected data.

Preview (0.x). The API may still change between minor versions. Proprietary: see LICENSE.

Quick start

1. Register your app in Clearly.HUB (on your app's page, no code):

SettingValue
Go to apphttps://app.example.com/clearly-hub/sign-in
Sign-in client (H2M): callback URLshttps://app.example.com/clearly-hub/callback, https://app.example.com/clearly-hub/answer, https://hub.clearly.app/components
Sign-in client: sign-out URLshttps://app.example.com, https://hub.clearly.app/components
M2M clientcreate it, keep the secret on your server
Plansgive each your own plan id as its External reference, for example pro

2. Mount the server handler (Node 24+):

import express from 'express';
import { clearlyHub } from '@clearly-hub/node';

const hub = clearlyHub({
  clientId: process.env.CLEARLY_HUB_CLIENT_ID,
  m2mClientId: process.env.CLEARLY_HUB_M2M_CLIENT_ID,
  m2mClientSecret: process.env.CLEARLY_HUB_M2M_CLIENT_SECRET,
  appId: process.env.CLEARLY_HUB_APP_ID, // your app's id in Clearly.HUB
  appUrl: 'https://app.example.com',
  secret: process.env.SESSION_SECRET, // 32+ random characters
});

const app = express();
app.use('/clearly-hub', hub.express());

Not Express? hub.handler takes a Request and returns a Response, for Next.js route handlers, Hono, Bun or Deno. Mount it at /clearly-hub/*.

3. Add the script and a button:

<!-- @0.1: follows 0.1.x fixes, not a breaking 0.2 -->
<script src="https://cdn.jsdelivr.net/npm/@clearly-hub/[email protected]/dist/clearly-hub.js"></script>

<clearly-hub-button action="sign-in"></clearly-hub-button>
<clearly-hub-button action="SELECT_DATA"></clearly-hub-button>

<script type="module">
  const session = await clearlyHub.session(); // { signedIn, user }
  window.addEventListener('clearly-hub-answer', (event) =>
    console.log(event.detail),
  );
</script>

Or npm install @clearly-hub/browser and import { clearlyHub } from '@clearly-hub/browser'. With React and TypeScript, add import '@clearly-hub/browser/react' once, so JSX knows <clearly-hub-button>.

The browser package

clearlyHub.session(){ signedIn: false }, or the signed-in user (sub, email, name, appUserId)
clearlyHub.signIn(), signOut()Go through Clearly.HUB and come back to this page
clearlyHub.signIn({ display: 'popup' })Sign in in a popup; this page stays. Resolves with the session. Also links a Clearly.HUB account to someone signed in with your own login
clearlyHub.open(action, { data, display })Opens a hosted screen; resolves with its checked answer. Call it from a click: browsers only allow popups there
clearlyHub.result()After a redirect screen: its answer, on the page it came back to
clearlyHub.getToken()The person's key for protected and paid data, or null when signed out. See Protected data
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
<clearly-hub-button action display data label>Branded button for sign-in, sign-out and each screen
clearly-hub-answer eventEvery answer, from a popup or after a redirect
clearly-hub-sign-in eventA sign-in popup is done; detail is the session

Styling the button. It uses your page's font. Attributes for the common cases: variant="outline", size="small" or size="large". CSS custom properties for your look: --clearly-hub-color, --clearly-hub-text-color, --clearly-hub-radius, --clearly-hub-padding, --clearly-hub-font. For anything else, style the real button through ::part(button):

clearly-hub-button {
  --clearly-hub-color: #0b7471;
  --clearly-hub-radius: 999px;
}
clearly-hub-button::part(button) {
  text-transform: uppercase;
}

Screens open in a popup by default (data-display="redirect" or "iframe" on the script tag changes that). A blocked popup falls back to a redirect.

In a dialog on your page (display: 'iframe'): the screen shows in a centred dialog and uses the person signed in to your app, so it doesn't sign in itself, the way the Google Drive picker works. A popup does the same. Nobody signed in with Clearly.HUB here: your app's sign-in comes first (for a popup, in that same window). Needs a Clearly.HUB release that allows this (preview). If your app has its own CSP: frame-src https://hub.clearly.app. Style the dialog with --clearly-hub-frame-width, --clearly-hub-frame-height, --clearly-hub-backdrop, or clearly-hub-frame::part(dialog | iframe | close).

ActionSend in dataanswer when result is SUCCESS
SELECT_SUBSCRIPTIONsubscription (_id, externalReference = your plan id), subscribedHub (_id, name), hasMultipleSubscriptions
SELECT_DATAoptionally { formats: ['geojson', 'wms'] } to offer only thoseactiveHub (_id, name), resources (datasetId, datasetTitle, resourceId, resourceName, url, format, accessibility, requiresToken, hasAccess)
REGISTER_RESOURCE{ url, format } of your result, at a public https URLactiveHub (hub id), dataset (datasetId, datasetTitle), resource (resourceId, resourceName)
SELECT_HUBhub (_id, name)
SELECT_DIGITAL_TWINactiveHub (_id, name), digitalTwin (_id, title, description, previewImage, ownerHub, findability, viewerUrl, resources as in SELECT_DATA plus selected and subLayers (name, selected))

Every answer also has user (_id, email): who answered, set by Clearly.HUB. Its _id is the person's sub, the same as session().user.sub.

Protected data

A protected or paid resource from SELECT_DATA (requiresToken: true) has a url on Clearly.HUB, which checks the person's key on every request. Send it as the Authorization header. In the page:

const response = await fetch(resource.url, {
  headers: { Authorization: await clearlyHub.getToken() },
});

Map layers (WMS, WMTS, tiles) load many URLs themselves: use a Leaflet, OpenLayers or Cesium option or plugin that adds headers to each request. The key lasts an hour; getToken() gets a new one in time, so call it again rather than keeping the value. On your server, hub.fetchAsUser(req, url) does the same.

The key is the person's own Clearly.HUB key: send it only to resource URLs from SELECT_DATA (Clearly.HUB's data proxy), and keep scripts you don't trust off pages that use it. fetchAsUser refuses other URLs.

The Node package

hub.express(), hub.handlerThe routes: /sign-in, /callback, /sign-out, /session, /open, /answer, /result, /token
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
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
hub.getUser(req)Who's signed in, in your own routes
hub.fetchAsUser(req, url)Fetch protected or paid data (from SELECT_DATA) on your server, with the person's key. Only URLs on Clearly.HUB's data proxy
hub.getSubscription(hubId), getSubscriptions()A hub's subscription to your app, or all of them
hub.getUserContext(req)The subscribed hubs the signed-in person is in, with their role (ADMIN or MEMBER) and plan; null when nobody is signed in
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
hub.datasets, hub.resources, hub.authMethodsRegister data in a hub, as the signed-in person or as your app; see Registering data
hub.getProxyContext(req, { audience })In an API behind Clearly.HUB's data proxy: who calls (sub, subType), for which hub, with which role, and the subscription. null without the proxy's header; see Behind the data proxy
hub.getUserToken(req), getAppToken()The person's key and your app's key, for calls of your own

Behind the data proxy

When your API is a protected resource, Clearly.HUB's data proxy can tell it who calls: it adds an x-oup-context header, a JWT signed with Clearly.HUB's key. getProxyContext checks the signature, the expiry and, with audience (your resource id), that it was made for this resource:

app.get('/api/flood-depth', async (req, res) => {
  const context = await hub.getProxyContext(req, { audience: resourceId });
  if (!context) return res.sendStatus(401);
  if (context.hub.role !== 'ADMIN') return res.sendStatus(403);
  // context.sub, context.subscription?.id, context.activeHub, ...
});

It throws a ClearlyHubError with code invalid-context when the header is there but doesn't verify.

The header only proves who calls if the request really came through the proxy. Check the proxy's own secret too, the resource's authentication method (for example a CUSTOM_HEADER), or accept traffic from the proxy only. The proxy drops an x-oup-context a client sends itself. For now the proxy adds the header only when the resource's authentication method has forwardSubscriptionId on.

Your own session in the hooks. With hub.express(), the hooks get Express's req as context.req, so they can use your own session (for example express-session) alongside Clearly.HUB's:

const hub = clearlyHub<express.Request>({
  // ...
  onSignIn: (person, { req }) => {
    const user = findOrCreateUser(person);
    req!.session.userId = user.id;
    return { appUserId: user.id };
  },
});

What it checks for you: every screen answer's Ed25519 signature, that it answers a screen opened in this browser, once (state), and that it was given to your app and that screen (client_id, callback URL, action).

What to trust in an answer: it's a snapshot of the moment the person clicked. For access later, ask getSubscription(hubId) when it's used. Who's signed in is the session's user.

The session is encrypted in cookies, so there's nothing to store; it's split over several cookies because tokens are large. A copied cookie stays valid until its refresh token expires: signing out clears only this browser's copy.

Local Clearly.HUB: pass urls: { screens, api, proxy, auth, issuer }; see examples/express/.env.example.

Another path than /clearly-hub, for example /auth/hub: set it in all three places.

  • Server: clearlyHub({ basePath: '/auth/hub', ... }) and mount it there, app.use('/auth/hub', hub.express()). The handler only answers under basePath and builds its callback URLs from it, so mounting alone isn't enough.
  • Page: data-base-path="/auth/hub" on the script tag, or clearlyHub.configure({ basePath: '/auth/hub' }).
  • Clearly.HUB: the sign-in client's callback URLs become {app}/auth/hub/callback and {app}/auth/hub/answer, and Go to app {app}/auth/hub/sign-in.

Registering data

Register your app's results in a hub from your own UI, without the REGISTER_RESOURCE screen: datasets, their resources, and the authentication methods protected resources use. Server only.

Every call starts with who acts: a request, for the person signed in on it, or 'app', for your app's own key (m2mClientId/m2mClientSecret), for example after a long job when nobody is signed in. A request without anyone signed in fails with UNAUTHENTICATED.

const { datasetId } = await hub.datasets.create(req, {
  title: 'Flood depth',
  ownerHubId,
  findability: 'LIMITED',
});

// Protected data: an authentication method first, then a resource behind the data proxy
const method = await hub.authMethods.create(req, ownerHubId, {
  name: 'Our tile server',
  type: 'CUSTOM_HEADER',
  authData: { parameterName: 'X-Api-Key', parameterValue: apiKey },
});
const resource = await hub.resources.add(req, datasetId, {
  name: 'Flood depth (WMS)',
  format: 'wms',
  sourceUrl: 'https://tiles.example.com/wms',
  authenticationMethodId: method.authMethodId,
});
CallReturns
hub.datasets.create(actor, input){ datasetId }
hub.datasets.update(actor, datasetId, input){ datasetId }
hub.datasets.delete(actor, datasetId)nothing
hub.datasets.get(actor, datasetId, { activeHubId? })the dataset, with resources
hub.resources.add(actor, datasetId, input)the resource
hub.resources.update(actor, datasetId, resourceId, input)the resource
hub.resources.delete(actor, datasetId, resourceId)nothing
hub.authMethods.list(actor, hubId, { type?, activeHubId? })the hub's methods
hub.authMethods.create(actor, hubId, input)the method
hub.authMethods.update(actor, hubId, authMethodId, input)the method
hub.authMethods.delete(actor, hubId, authMethodId)nothing

Who may. Clearly.HUB decides. A person needs to be ADMIN of the hub (for datasets and resources: the dataset's owner hub). Your app's key has its own roles: admin of the hub that owns your app and its sub-hubs, member of that hub's parents, and editor in hubs that subscribed to your app (and their sub-hubs).

CallPersonYour app: owner hub and sub-hubsIts parentsHubs that subscribed
datasets: create, update, deleteADMINyesnoyes
datasets: get, publicanyoneyesyesyes
datasets: get, LIMITEDin a hub that may find ityesyesif one may find it
resources: add, update, deleteADMINyesnoyes
authMethods: create, update, deleteADMINyesnono
authMethods: listADMINyesnono

Moving a dataset to another ownerHubId also needs the person or app to be in that hub. Market masters can't make changes.

Good to know

  • datasets.update and resources.update send everything, as create does: what you leave out may be cleared (spatial is). Start from datasets.get(), which returns the shape update takes. spatial is stored on update only, not on create.
  • A protected resource has an authenticationMethodId and a sourceUrl (the address behind the proxy), and no url: Clearly.HUB makes it, on its data proxy. An open resource has a url only. Paid data also has a subscriptionModelId, a plan of the owner hub. format is one of Clearly.HUB's formats (wms, wfs, wmts, tilejson, api, ...). URLs need a domain: localhost is refused, use lvh.me.
  • authData holds secrets. Clearly.HUB never gives it back, and update replaces all of it. Fields per type: BASIC_AUTH username, password; URL_PARAMETER and CUSTOM_HEADER parameterName, parameterValue; ISHARE_AUTH_METHOD tokenEndpoint, serviceProviderEORI, entitledPartyEORI; JWT_ENDPOINT tokenEndpoint, username, password, usernameField, passwordField (and accessTokenProperty, refreshTokenProperty); S3 s3AccessKeyId, s3SecretAccessKey; OAUTH2_CLIENT_CREDENTIALS tokenEndpoint, clientId, clientSecret (and accessTokenProperty, secretAsHeader, scope). The types (AuthDataByType) say the same.
  • Deleting a dataset deletes its resources and cancels subscriptions to its paid ones.
  • Errors are ClearlyHubErrors with Clearly.HUB's message and code (BAD_USER_INPUT, FORBIDDEN, UNAUTHENTICATED). The SDK checks what it can before sending, with the same code.

Not covered (yet)

  • Setting up your own workspaces or tenants: do it by hand, with plans that need approval, or in onAnswer for SELECT_SUBSCRIPTION.
  • Other server languages (.NET, Python, ...): the browser package works with any backend that answers the same routes. Contact us at [email protected] and we'll help you with yours.

Working on this repo

npm install
npm run build       # both packages
npm test            # vitest
npm run typecheck
npm run lint
npm run example     # examples/express on http://localhost:5175 (copy .env.example to .env first)

版本与发布记录

@clearly-hub/browser 共发布 18 个版本,首次发布于 2026/10/22;过去 12 个月发布了 18 个版本。

版本发布日期距今
0.12.0latest2026/10/3011 天前
0.11.02026/10/2614 天前
0.10.02026/10/2614 天前
0.9.02026/10/2614 天前
0.8.12026/10/2417 天前
0.8.02026/10/2417 天前

关于 @clearly-hub/browser 的常见问题

@clearly-hub/browser 是什么?

Clearly.HUB in the browser: sign-in, hosted screens and the session, talking to @clearly-hub/node

如何安装 @clearly-hub/browser?

在项目目录运行 npm install @clearly-hub/browser,或使用 pnpm add / yarn add / bun add。

@clearly-hub/browser 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@clearly-hub/browser 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@clearly-hub/browser 可以商用吗?

@clearly-hub/browser 使用 SEE LICENSE IN LICENSE 许可。许可条款写在包内的独立文件中,请阅读该文件确认可否商用。以上为概括,不构成法律意见,请以许可原文为准。

@clearly-hub/browser 的下载量有多少?

根据 npm 官方下载统计,@clearly-hub/browser 在 2026/10/10 – 2026/11/08 这 30 天内被下载 3,191 次,日均约 106 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@clearly-hub/browser 还在维护吗?

最新版本 0.12.0 发布于 2026/10/30(11 天前);至今共发布 18 个版本,其中过去 12 个月发布了 18 个;首次发布于 2026/10/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。