@clearly-hub/node 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  Connect your web app to Clearly.HUB with a script, some config and one server handler. You get
4
4
  "Sign in with Clearly.HUB", the hosted screens (choose a plan, pick data, save a result, choose a
5
- hub) and each hub's subscription to your app. Tokens stay on your server.
5
+ hub) and each hub's subscription to your app. The refresh token stays on your server; the page gets
6
+ the access token only when it asks, for protected data.
6
7
 
7
8
  > **Preview (0.x).** The API may still change between minor versions. Proprietary: see
8
9
  > [LICENSE](LICENSE).
@@ -51,7 +52,9 @@ Hono, Bun or Deno. Mount it at `/clearly-hub/*`.
51
52
 
52
53
  <script type="module">
53
54
  const session = await clearlyHub.session(); // { signedIn, user }
54
- window.addEventListener('clearly-hub-answer', (event) => console.log(event.detail));
55
+ window.addEventListener('clearly-hub-answer', (event) =>
56
+ console.log(event.detail),
57
+ );
55
58
  </script>
56
59
  ```
57
60
 
@@ -59,14 +62,16 @@ Or `npm install @clearly-hub/browser` and `import { clearlyHub } from '@clearly-
59
62
 
60
63
  ## The browser package
61
64
 
62
- | | |
63
- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
64
- | `clearlyHub.session()` | `{ signedIn: false }`, or the signed-in user (`sub`, `email`, `name`, `appUserId`) |
65
- | `clearlyHub.signIn()`, `signOut()` | Go through Clearly.HUB and come back to this page |
66
- | `clearlyHub.open(action, { data, display })` | Opens a hosted screen; resolves with its checked answer. Call it from a click: browsers only allow popups there |
67
- | `clearlyHub.result()` | After a redirect screen: its answer, on the page it came back to |
68
- | `<clearly-hub-button action display data label>` | Branded button for `sign-in`, `sign-out` and each screen |
69
- | `clearly-hub-answer` event | Every answer, from a popup or after a redirect |
65
+ | | |
66
+ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
67
+ | `clearlyHub.session()` | `{ signedIn: false }`, or the signed-in user (`sub`, `email`, `name`, `appUserId`) |
68
+ | `clearlyHub.signIn()`, `signOut()` | Go through Clearly.HUB and come back to this page |
69
+ | `clearlyHub.open(action, { data, display })` | Opens a hosted screen; resolves with its checked answer. Call it from a click: browsers only allow popups there |
70
+ | `clearlyHub.result()` | After a redirect screen: its answer, on the page it came back to |
71
+ | `clearlyHub.getToken()` | The person's key for protected and paid data, or `null` when signed out. See [Protected data](#protected-data) |
72
+ | `clearlyHub.onAnswer(callback)` | Every answer, once each, from a popup or after a redirect; also when you start listening after it arrived (React). Returns a function that stops listening |
73
+ | `<clearly-hub-button action display data label>` | Branded button for `sign-in`, `sign-out` and each screen |
74
+ | `clearly-hub-answer` event | Every answer, from a popup or after a redirect |
70
75
 
71
76
  **Styling the button.** It uses your page's font. Attributes for the common cases:
72
77
  `variant="outline"`, `size="small"` or `size="large"`. CSS custom properties for your look:
@@ -93,15 +98,33 @@ blocked popup falls back to a redirect.
93
98
  | `REGISTER_RESOURCE` | `{ url, format }` of your result, at a public https URL | `activeHub`, `dataset`, `resource` |
94
99
  | `SELECT_HUB` | | `hub` (`_id`, `name`) |
95
100
 
101
+ ## Protected data
102
+
103
+ A protected or paid resource from `DATA_PICKER` (`requiresToken: true`) has a `url` on Clearly.HUB,
104
+ which checks the person's key on every request. Send it as the `Authorization` header. In the page:
105
+
106
+ ```js
107
+ const response = await fetch(resource.url, {
108
+ headers: { Authorization: await clearlyHub.getToken() },
109
+ });
110
+ ```
111
+
112
+ Map layers (WMS, WMTS, tiles) load many URLs themselves: use a Leaflet, OpenLayers or Cesium option
113
+ or plugin that adds headers to each request. The key lasts an hour; `getToken()` gets a new one in
114
+ time, so call it again rather than keeping the value. On your server, `hub.fetchAsUser(req, url)`
115
+ does the same.
116
+
117
+ The key is the person's own Clearly.HUB key: keep scripts you don't trust off pages that use it.
118
+
96
119
  ## The Node package
97
120
 
98
121
  | | |
99
122
  | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
- | `hub.express()`, `hub.handler` | The routes: `/sign-in`, `/callback`, `/sign-out`, `/session`, `/open`, `/answer`, `/result` |
123
+ | `hub.express()`, `hub.handler` | The routes: `/sign-in`, `/callback`, `/sign-out`, `/session`, `/open`, `/answer`, `/result`, `/token` |
101
124
  | `onSignIn(person, { request, req })` | Called after each sign-in. Return your own user id to keep it in the session, or `{ appUserId, redirectTo }` to also choose the next page |
102
125
  | `onAnswer(result, { user, data, request, req })` | Called with each checked answer before it reaches the browser, for example to set up a workspace. `data` is what the screen was sent; what you return reaches the page as `result.app` |
103
126
  | `hub.getUser(req)` | Who's signed in, in your own routes |
104
- | `hub.fetchAsUser(req, url)` | Fetch protected or paid data (from `DATA_PICKER`) with the person's key |
127
+ | `hub.fetchAsUser(req, url)` | Fetch protected or paid data (from `DATA_PICKER`) on your server, with the person's key |
105
128
  | `hub.getSubscription(hubId)`, `getSubscriptions()` | A hub's subscription to your app, or all of them |
106
129
  | `hub.reportUsage({ request or sub, hubId, quantity })` | Report billable use; Clearly.HUB bills the hub. Pass `sub` for a Clearly.HUB account you stored, without a live sign-in |
107
130
  | `hub.getUserToken(req)`, `getAppToken()` | The person's key and your app's key, for calls of your own |
@@ -131,7 +154,10 @@ store; it's split over several cookies because tokens are large.
131
154
 
132
155
  - Setting up your own workspaces or tenants: do it by hand, with plans that need approval, or in
133
156
  `onAnswer` for `SELECT_SUBSCRIPTION`.
134
- - Other server languages: follow the Build checklist.
157
+ - Other server languages (.NET, Python, ...): the browser package works with any
158
+ backend that answers the same routes. Contact us at
159
+ [support@futureinsight.nl](mailto:support@futureinsight.nl) and we'll help
160
+ you with yours.
135
161
 
136
162
  ## Working on this repo
137
163
 
package/dist/index.cjs CHANGED
@@ -34,6 +34,23 @@ __export(index_exports, {
34
34
  });
35
35
  module.exports = __toCommonJS(index_exports);
36
36
 
37
+ // ../shared/src/protocol.ts
38
+ var DEFAULT_BASE_PATH = "/clearly-hub";
39
+ var DEFAULT_HUB_URL = "https://hub.clearly.app";
40
+ var DEFAULT_AUTH_URL = "https://auth.clearly.app";
41
+ var DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
42
+ var ROUTES = {
43
+ signIn: "/sign-in",
44
+ callback: "/callback",
45
+ signOut: "/sign-out",
46
+ session: "/session",
47
+ open: "/open",
48
+ answer: "/answer",
49
+ result: "/result",
50
+ token: "/token"
51
+ };
52
+ var ANSWER_CHANNEL = "clearly-hub-answers";
53
+
37
54
  // ../shared/src/screens.ts
38
55
  var SCREEN_ACTIONS = [
39
56
  "SELECT_SUBSCRIPTION",
@@ -62,22 +79,6 @@ function checkScreenData(action, data) {
62
79
  return void 0;
63
80
  }
64
81
 
65
- // ../shared/src/protocol.ts
66
- var DEFAULT_BASE_PATH = "/clearly-hub";
67
- var DEFAULT_HUB_URL = "https://hub.clearly.app";
68
- var DEFAULT_AUTH_URL = "https://auth.clearly.app";
69
- var DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
70
- var ROUTES = {
71
- signIn: "/sign-in",
72
- callback: "/callback",
73
- signOut: "/sign-out",
74
- session: "/session",
75
- open: "/open",
76
- answer: "/answer",
77
- result: "/result"
78
- };
79
- var ANSWER_CHANNEL = "clearly-hub-answers";
80
-
81
82
  // src/config.ts
82
83
  var trimSlash = (url) => url.replace(/\/+$/, "");
83
84
  function resolveConfig(options) {
@@ -120,11 +121,15 @@ function toNodeMiddleware(handle, appUrl) {
120
121
  try {
121
122
  const headers = new Headers();
122
123
  for (const [name, value] of Object.entries(req.headers)) {
123
- if (Array.isArray(value)) value.forEach((item) => headers.append(name, item));
124
+ if (Array.isArray(value))
125
+ value.forEach((item) => headers.append(name, item));
124
126
  else if (value !== void 0) headers.set(name, value);
125
127
  }
126
128
  const url = new URL(req.originalUrl ?? req.url ?? "/", appUrl);
127
- const response = await handle(new Request(url, { method: req.method, headers }), req);
129
+ const response = await handle(
130
+ new Request(url, { method: req.method, headers }),
131
+ req
132
+ );
128
133
  if (response.headers.get("X-Clearly-Hub") === "unhandled" && next) {
129
134
  next();
130
135
  return;
@@ -178,16 +183,24 @@ var Screens = class {
178
183
  base64Decode(signature),
179
184
  new TextEncoder().encode(data)
180
185
  );
181
- if (!isSigned) throw new AnswerError("This answer was not signed by Clearly.HUB");
182
- const { request, response } = JSON.parse(new TextDecoder().decode(base64Decode(data)));
186
+ if (!isSigned)
187
+ throw new AnswerError("This answer was not signed by Clearly.HUB");
188
+ const { request, response } = JSON.parse(
189
+ new TextDecoder().decode(base64Decode(data))
190
+ );
183
191
  return { state: request.state, response };
184
192
  }
185
193
  // Fetched once; a key change needs a restart
186
194
  getPublicKey() {
187
195
  this.publicKey ??= this.config.fetch(this.config.urls.publicKey).then((response) => {
188
- if (!response.ok) throw new Error(`No public key at ${this.config.urls.publicKey}`);
196
+ if (!response.ok)
197
+ throw new Error(`No public key at ${this.config.urls.publicKey}`);
189
198
  return response.text();
190
- }).then((pem) => crypto.subtle.importKey("spki", pemToDer(pem), "Ed25519", false, ["verify"])).catch((error) => {
199
+ }).then(
200
+ (pem) => crypto.subtle.importKey("spki", pemToDer(pem), "Ed25519", false, [
201
+ "verify"
202
+ ])
203
+ ).catch((error) => {
191
204
  this.publicKey = void 0;
192
205
  throw error;
193
206
  });
@@ -198,7 +211,8 @@ var AnswerError = class extends Error {
198
211
  };
199
212
  function toScreenResult(action, response) {
200
213
  const { result, ...answer } = response;
201
- if (result === "SUCCESS") return { action, result: "SUCCESS", answer };
214
+ if (result === "SUCCESS")
215
+ return { action, result: "SUCCESS", answer };
202
216
  return { action, result: result === "FAILURE" ? "FAILURE" : "CANCELLED" };
203
217
  }
204
218
  function base64Encode(text) {
@@ -214,7 +228,11 @@ function readRawQuery(url) {
214
228
  const params = /* @__PURE__ */ new Map();
215
229
  for (const pair of url.search.slice(1).split("&")) {
216
230
  const index = pair.indexOf("=");
217
- if (index > 0) params.set(pair.slice(0, index), decodeURIComponent(pair.slice(index + 1)));
231
+ if (index > 0)
232
+ params.set(
233
+ pair.slice(0, index),
234
+ decodeURIComponent(pair.slice(index + 1))
235
+ );
218
236
  }
219
237
  return params;
220
238
  }
@@ -228,7 +246,8 @@ function createHandler(parts) {
228
246
  const url = new URL(request.url);
229
247
  if (!url.pathname.startsWith(config.basePath)) return notFound();
230
248
  const route = url.pathname.slice(config.basePath.length) || "/";
231
- if (request.method !== "GET") return new Response("Method not allowed", { status: 405 });
249
+ if (request.method !== "GET")
250
+ return new Response("Method not allowed", { status: 405 });
232
251
  const host = request.headers.get("host");
233
252
  const appHost = new URL(config.appUrl).host;
234
253
  if (host && host !== appHost && (route === ROUTES.signIn || route === ROUTES.open)) {
@@ -241,7 +260,8 @@ function createHandler(parts) {
241
260
  response = await routeRequest(route, url, data, parts, { request, req });
242
261
  } catch (error) {
243
262
  const isRefusedAnswer = error instanceof AnswerError;
244
- if (!isRefusedAnswer) console.error(`Clearly.HUB: ${route} failed:`, error);
263
+ if (!isRefusedAnswer)
264
+ console.error(`Clearly.HUB: ${route} failed:`, error);
245
265
  response = errorPage(isRefusedAnswer ? 400 : 500, describeError(error));
246
266
  }
247
267
  if (JSON.stringify(data) !== before) {
@@ -259,7 +279,8 @@ async function routeRequest(route, url, session, parts, incoming) {
259
279
  case ROUTES.callback:
260
280
  return finishSignIn(url, session, parts, incoming);
261
281
  case ROUTES.signOut:
262
- for (const key of Object.keys(session)) delete session[key];
282
+ for (const key of Object.keys(session))
283
+ delete session[key];
263
284
  return redirect(parts.signIn.signOutUrl());
264
285
  case ROUTES.session:
265
286
  return json(await readSession(session, parts));
@@ -272,12 +293,16 @@ async function routeRequest(route, url, session, parts, incoming) {
272
293
  delete session.result;
273
294
  return json(result);
274
295
  }
296
+ case ROUTES.token:
297
+ return readToken(session, parts);
275
298
  default:
276
299
  return notFound();
277
300
  }
278
301
  }
279
302
  async function startSignIn(url, session, { config, signIn }) {
280
- const { url: authorizationUrl, pending } = await signIn.start(safeReturnTo(url, config));
303
+ const { url: authorizationUrl, pending } = await signIn.start(
304
+ safeReturnTo(url, config)
305
+ );
281
306
  session.signIn = pending;
282
307
  return redirect(authorizationUrl);
283
308
  }
@@ -291,13 +316,28 @@ async function finishSignIn(url, session, { config, signIn }, incoming) {
291
316
  const { appUserId, redirectTo } = typeof decision === "string" ? { appUserId: decision, redirectTo: void 0 } : decision ?? {};
292
317
  session.user = { ...person, appUserId };
293
318
  session.tokens = tokens;
294
- return redirect(`${config.appUrl}${safePath(redirectTo, config) ?? pending.returnTo}`);
319
+ return redirect(
320
+ `${config.appUrl}${safePath(redirectTo, config) ?? pending.returnTo}`
321
+ );
295
322
  }
296
323
  async function readSession(session, parts) {
297
324
  const accessToken = await getFreshAccessToken(session, parts);
298
325
  if (!session.user || !accessToken) return { signedIn: false };
299
326
  return { signedIn: true, user: session.user };
300
327
  }
328
+ async function readToken(session, parts) {
329
+ const accessToken = await getFreshAccessToken(session, parts);
330
+ if (!session.user || !accessToken)
331
+ return json({ error: "Nobody is signed in with Clearly.HUB" }, 401);
332
+ const token = {
333
+ accessToken,
334
+ expiresIn: Math.max(
335
+ 0,
336
+ session.tokens.expiresAt - Math.floor(Date.now() / 1e3)
337
+ )
338
+ };
339
+ return json(token);
340
+ }
301
341
  async function getFreshAccessToken(session, { signIn }) {
302
342
  const tokens = session.tokens;
303
343
  if (!tokens) return void 0;
@@ -314,7 +354,8 @@ async function getFreshAccessToken(session, { signIn }) {
314
354
  }
315
355
  function openScreen(url, session, { config, screens }) {
316
356
  const action = url.searchParams.get("action");
317
- if (!isScreenAction(action)) return errorPage(400, `Unknown screen: ${action}`);
357
+ if (!isScreenAction(action))
358
+ return errorPage(400, `Unknown screen: ${action}`);
318
359
  const display = url.searchParams.get("display") === "redirect" ? "redirect" : "popup";
319
360
  const dataParam = url.searchParams.get("data");
320
361
  let data;
@@ -345,7 +386,9 @@ async function receiveAnswer(url, session, { config, screens }, incoming) {
345
386
  );
346
387
  const pending = state ? session.screens?.[state] : void 0;
347
388
  if (!pending || Date.now() - pending.createdAt > SCREEN_TTL_MS) {
348
- throw new AnswerError("This answer does not belong to a screen opened in this browser");
389
+ throw new AnswerError(
390
+ "This answer does not belong to a screen opened in this browser"
391
+ );
349
392
  }
350
393
  delete session.screens[state];
351
394
  let result = toScreenResult(pending.action, response);
@@ -357,7 +400,11 @@ async function receiveAnswer(url, session, { config, screens }, incoming) {
357
400
  });
358
401
  if (app !== void 0) result = { ...result, app };
359
402
  } catch (error) {
360
- result = { action: pending.action, result: "FAILURE", error: error.message };
403
+ result = {
404
+ action: pending.action,
405
+ result: "FAILURE",
406
+ error: error.message
407
+ };
361
408
  }
362
409
  if (pending.display === "redirect") {
363
410
  session.result = { ref: pending.ref, result };
@@ -392,11 +439,21 @@ function safePath(path, config) {
392
439
  }
393
440
  }
394
441
  var redirect = (location) => new Response(null, { status: 302, headers: { Location: location } });
395
- var json = (body) => new Response(JSON.stringify(body), {
396
- headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
442
+ var json = (body, status = 200) => new Response(JSON.stringify(body), {
443
+ status,
444
+ headers: {
445
+ "Content-Type": "application/json",
446
+ "Cache-Control": "no-store"
447
+ }
448
+ });
449
+ var html = (body, status = 200) => new Response(body, {
450
+ status,
451
+ headers: { "Content-Type": "text/html; charset=utf-8" }
452
+ });
453
+ var notFound = () => new Response("Not found", {
454
+ status: 404,
455
+ headers: { "X-Clearly-Hub": "unhandled" }
397
456
  });
398
- var html = (body, status = 200) => new Response(body, { status, headers: { "Content-Type": "text/html; charset=utf-8" } });
399
- var notFound = () => new Response("Not found", { status: 404, headers: { "X-Clearly-Hub": "unhandled" } });
400
457
  function describeError(error) {
401
458
  const messages = [];
402
459
  for (let current = error; current instanceof Error && messages.length < 5; current = current.cause) {
@@ -405,7 +462,10 @@ function describeError(error) {
405
462
  return messages.join(": ") || String(error);
406
463
  }
407
464
  function errorPage(status, message) {
408
- const escaped = message.replace(/[&<>"]/g, (char) => `&#${char.charCodeAt(0)};`);
465
+ const escaped = message.replace(
466
+ /[&<>"]/g,
467
+ (char) => `&#${char.charCodeAt(0)};`
468
+ );
409
469
  return html(
410
470
  `<!doctype html><meta charset="utf-8"><title>Clearly.HUB</title><p>${escaped}</p>`,
411
471
  status
@@ -427,7 +487,9 @@ var HubApi = class {
427
487
  try {
428
488
  return await this.config.fetch(url, init);
429
489
  } catch (error) {
430
- throw new Error(`Clearly.HUB could not be reached at ${url}`, { cause: error });
490
+ throw new Error(`Clearly.HUB could not be reached at ${url}`, {
491
+ cause: error
492
+ });
431
493
  }
432
494
  }
433
495
  get hasAppKey() {
@@ -437,11 +499,16 @@ var HubApi = class {
437
499
  // until shortly before it expires
438
500
  async getAppToken() {
439
501
  const now = Date.now() / 1e3;
440
- if (this.appToken && this.appToken.expiresAt > now + 60) return this.appToken.value;
502
+ if (this.appToken && this.appToken.expiresAt > now + 60)
503
+ return this.appToken.value;
441
504
  if (!this.hasAppKey) {
442
- throw new Error("Clearly.HUB: m2mClientId and m2mClientSecret are needed for this");
505
+ throw new Error(
506
+ "Clearly.HUB: m2mClientId and m2mClientSecret are needed for this"
507
+ );
443
508
  }
444
- const credentials = btoa(`${this.config.m2mClientId}:${this.config.m2mClientSecret}`);
509
+ const credentials = btoa(
510
+ `${this.config.m2mClientId}:${this.config.m2mClientSecret}`
511
+ );
445
512
  const response = await this.call(`${this.config.urls.auth}/oauth2/token`, {
446
513
  method: "POST",
447
514
  headers: {
@@ -457,7 +524,10 @@ var HubApi = class {
457
524
  throw new Error(`Clearly.HUB refused the app's key (${response.status})`);
458
525
  }
459
526
  const body = await response.json();
460
- this.appToken = { value: body.access_token, expiresAt: now + body.expires_in };
527
+ this.appToken = {
528
+ value: body.access_token,
529
+ expiresAt: now + body.expires_in
530
+ };
461
531
  return body.access_token;
462
532
  }
463
533
  // Every hub that bought your app, with status and your plan id
@@ -465,11 +535,16 @@ var HubApi = class {
465
535
  if (this.subscriptions && Date.now() - this.subscriptions.fetchedAt < SUBSCRIPTIONS_CACHE_MS) {
466
536
  return this.subscriptions.value;
467
537
  }
468
- const response = await this.call(`${this.config.urls.api}/api/app-api/subscriptions`, {
469
- headers: { Authorization: `Bearer ${await this.getAppToken()}` }
470
- });
538
+ const response = await this.call(
539
+ `${this.config.urls.api}/api/app-api/subscriptions`,
540
+ {
541
+ headers: { Authorization: `Bearer ${await this.getAppToken()}` }
542
+ }
543
+ );
471
544
  if (!response.ok) {
472
- throw new Error(`Could not read the subscriptions from Clearly.HUB (${response.status})`);
545
+ throw new Error(
546
+ `Could not read the subscriptions from Clearly.HUB (${response.status})`
547
+ );
473
548
  }
474
549
  const items = await response.json();
475
550
  const value = items.map((item) => ({
@@ -502,12 +577,16 @@ var HubApi = class {
502
577
  });
503
578
  if (!response.ok) {
504
579
  const body = await response.json().catch(() => ({}));
505
- throw new Error(`Clearly.HUB did not accept the usage: ${body.message ?? response.status}`);
580
+ throw new Error(
581
+ `Clearly.HUB did not accept the usage: ${body.message ?? response.status}`
582
+ );
506
583
  }
507
584
  }
508
585
  };
509
586
  function pickSubscription(subscriptions, hubId) {
510
- const ofHub = subscriptions.filter((subscription) => subscription.hubId === hubId);
587
+ const ofHub = subscriptions.filter(
588
+ (subscription) => subscription.hubId === hubId
589
+ );
511
590
  return ofHub.find((s) => s.status === "ACTIVE") ?? ofHub.find((s) => s.status === "PENDING");
512
591
  }
513
592
 
@@ -524,7 +603,12 @@ var SignIn = class {
524
603
  }
525
604
  // Discovery happens once, on the first sign-in
526
605
  getConfiguration() {
527
- this.configuration ??= oidc.discovery(new URL(this.config.urls.issuer), this.config.clientId, void 0, oidc.None()).catch((error) => {
606
+ this.configuration ??= oidc.discovery(
607
+ new URL(this.config.urls.issuer),
608
+ this.config.clientId,
609
+ void 0,
610
+ oidc.None()
611
+ ).catch((error) => {
528
612
  this.configuration = void 0;
529
613
  throw error;
530
614
  });
@@ -546,11 +630,15 @@ var SignIn = class {
546
630
  return { url: url.href, pending: { codeVerifier, state, nonce, returnTo } };
547
631
  }
548
632
  async finish(currentUrl, pending) {
549
- const tokens = await oidc.authorizationCodeGrant(await this.getConfiguration(), currentUrl, {
550
- pkceCodeVerifier: pending.codeVerifier,
551
- expectedState: pending.state,
552
- expectedNonce: pending.nonce
553
- });
633
+ const tokens = await oidc.authorizationCodeGrant(
634
+ await this.getConfiguration(),
635
+ currentUrl,
636
+ {
637
+ pkceCodeVerifier: pending.codeVerifier,
638
+ expectedState: pending.state,
639
+ expectedNonce: pending.nonce
640
+ }
641
+ );
554
642
  const claims = tokens.claims();
555
643
  if (!claims) throw new Error("Clearly.HUB sent no ID token");
556
644
  return {
@@ -563,7 +651,10 @@ var SignIn = class {
563
651
  };
564
652
  }
565
653
  async refresh(refreshToken) {
566
- const tokens = await oidc.refreshTokenGrant(await this.getConfiguration(), refreshToken);
654
+ const tokens = await oidc.refreshTokenGrant(
655
+ await this.getConfiguration(),
656
+ refreshToken
657
+ );
567
658
  return toSessionTokens(tokens, refreshToken);
568
659
  }
569
660
  // Signing out of Clearly.HUB too, or the next sign-in goes straight through
@@ -596,12 +687,16 @@ function parseCookies(header) {
596
687
  const cookies = /* @__PURE__ */ new Map();
597
688
  for (const part of header.split(";")) {
598
689
  const index = part.indexOf("=");
599
- if (index > 0) cookies.set(part.slice(0, index).trim(), part.slice(index + 1).trim());
690
+ if (index > 0)
691
+ cookies.set(part.slice(0, index).trim(), part.slice(index + 1).trim());
600
692
  }
601
693
  return cookies;
602
694
  }
603
695
  async function deriveKey(secret) {
604
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
696
+ const digest = await crypto.subtle.digest(
697
+ "SHA-256",
698
+ new TextEncoder().encode(secret)
699
+ );
605
700
  return new Uint8Array(digest);
606
701
  }
607
702
  var SessionStore = class {
@@ -619,8 +714,14 @@ var SessionStore = class {
619
714
  }
620
715
  if (!chunks.length) return { data: {}, chunkCount: 0 };
621
716
  try {
622
- const { plaintext } = await (0, import_jose.compactDecrypt)(chunks.join(""), await this.keyPromise);
623
- return { data: JSON.parse(new TextDecoder().decode(plaintext)), chunkCount: chunks.length };
717
+ const { plaintext } = await (0, import_jose.compactDecrypt)(
718
+ chunks.join(""),
719
+ await this.keyPromise
720
+ );
721
+ return {
722
+ data: JSON.parse(new TextDecoder().decode(plaintext)),
723
+ chunkCount: chunks.length
724
+ };
624
725
  } catch {
625
726
  return { data: {}, chunkCount: chunks.length };
626
727
  }
@@ -628,7 +729,9 @@ var SessionStore = class {
628
729
  // Set-Cookie headers for the new session, clearing chunks it no longer needs
629
730
  async write(data, previousChunkCount) {
630
731
  const isEmpty = Object.keys(data).length === 0;
631
- const chunks = isEmpty ? [] : (await this.encrypt(data)).match(new RegExp(`.{1,${CHUNK_SIZE}}`, "g"));
732
+ const chunks = isEmpty ? [] : (await this.encrypt(data)).match(
733
+ new RegExp(`.{1,${CHUNK_SIZE}}`, "g")
734
+ );
632
735
  const attributes = `Path=/; HttpOnly; SameSite=Lax${this.config.isSecure ? "; Secure" : ""}`;
633
736
  const headers = chunks.map(
634
737
  (chunk, i) => `${COOKIE_NAME}.${i}=${chunk}; Max-Age=${MAX_AGE_SECONDS}; ${attributes}`
@@ -678,7 +781,8 @@ function clearlyHub(options) {
678
781
  // person's key. Open data works without it.
679
782
  async fetchAsUser(request, url, init = {}) {
680
783
  const token = await getUserToken(request);
681
- if (!token) throw new Error("Clearly.HUB: nobody is signed in with Clearly.HUB");
784
+ if (!token)
785
+ throw new Error("Clearly.HUB: nobody is signed in with Clearly.HUB");
682
786
  const headers = new Headers(init.headers);
683
787
  headers.set("Authorization", token);
684
788
  return config.fetch(url, { ...init, headers });
@@ -703,11 +807,18 @@ function clearlyHub(options) {
703
807
  }) {
704
808
  const person = sub ?? (request && (await parts.sessions.read(request)).data.user?.sub);
705
809
  if (!person) {
706
- throw new Error("Clearly.HUB: reportUsage needs the sub of a Clearly.HUB account");
810
+ throw new Error(
811
+ "Clearly.HUB: reportUsage needs the sub of a Clearly.HUB account"
812
+ );
707
813
  }
708
- const subscription = pickSubscription(await parts.api.getSubscriptions(), hubId);
814
+ const subscription = pickSubscription(
815
+ await parts.api.getSubscriptions(),
816
+ hubId
817
+ );
709
818
  if (subscription?.status !== "ACTIVE") {
710
- throw new Error(`Clearly.HUB: hub ${hubId} has no active subscription to your app`);
819
+ throw new Error(
820
+ `Clearly.HUB: hub ${hubId} has no active subscription to your app`
821
+ );
711
822
  }
712
823
  await parts.api.reportUsage({
713
824
  sub: person,
package/dist/index.d.cts CHANGED
@@ -71,6 +71,26 @@ type ScreenResult<A extends ScreenAction = ScreenAction> = {
71
71
  app?: unknown;
72
72
  };
73
73
 
74
+ declare const DEFAULT_BASE_PATH = "/clearly-hub";
75
+ declare const DEFAULT_HUB_URL = "https://hub.clearly.app";
76
+ declare const DEFAULT_AUTH_URL = "https://auth.clearly.app";
77
+ declare const DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
78
+ declare const ROUTES: {
79
+ readonly signIn: "/sign-in";
80
+ readonly callback: "/callback";
81
+ readonly signOut: "/sign-out";
82
+ readonly session: "/session";
83
+ readonly open: "/open";
84
+ readonly answer: "/answer";
85
+ readonly result: "/result";
86
+ readonly token: "/token";
87
+ };
88
+ declare const ANSWER_CHANNEL = "clearly-hub-answers";
89
+ interface AnswerMessage {
90
+ ref: string;
91
+ result: ScreenResult;
92
+ }
93
+
74
94
  interface SessionUser {
75
95
  sub: string;
76
96
  email: string;
@@ -89,24 +109,9 @@ type Session = {
89
109
  signedIn: true;
90
110
  user: SessionUser;
91
111
  };
92
-
93
- declare const DEFAULT_BASE_PATH = "/clearly-hub";
94
- declare const DEFAULT_HUB_URL = "https://hub.clearly.app";
95
- declare const DEFAULT_AUTH_URL = "https://auth.clearly.app";
96
- declare const DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
97
- declare const ROUTES: {
98
- readonly signIn: "/sign-in";
99
- readonly callback: "/callback";
100
- readonly signOut: "/sign-out";
101
- readonly session: "/session";
102
- readonly open: "/open";
103
- readonly answer: "/answer";
104
- readonly result: "/result";
105
- };
106
- declare const ANSWER_CHANNEL = "clearly-hub-answers";
107
- interface AnswerMessage {
108
- ref: string;
109
- result: ScreenResult;
112
+ interface AccessToken {
113
+ accessToken: string;
114
+ expiresIn: number;
110
115
  }
111
116
 
112
117
  interface SignedInPerson {
@@ -179,4 +184,4 @@ declare function clearlyHub<Req = unknown>(options: ClearlyHubOptions<Req>): {
179
184
  type ClearlyHub = ReturnType<typeof clearlyHub>;
180
185
 
181
186
  export { ANSWER_CHANNEL, DEFAULT_AUTH_URL, DEFAULT_BASE_PATH, DEFAULT_HUB_URL, DEFAULT_ISSUER, ROUTES, SCREEN_ACTIONS, checkScreenData, clearlyHub, isScreenAction };
182
- export type { AnswerContext, AnswerMessage, AppSubscription, ClearlyHub, ClearlyHubOptions, ClearlyHubUrls, CookieSource, HookContext, HubRef, HubSubscription, PickedResource, ScreenAction, ScreenAnswers, ScreenData, ScreenDisplay, ScreenResult, Session, SessionUser, SignInDecision, SignedInPerson };
187
+ export type { AccessToken, AnswerContext, AnswerMessage, AppSubscription, ClearlyHub, ClearlyHubOptions, ClearlyHubUrls, CookieSource, HookContext, HubRef, HubSubscription, PickedResource, ScreenAction, ScreenAnswers, ScreenData, ScreenDisplay, ScreenResult, Session, SessionUser, SignInDecision, SignedInPerson };
package/dist/index.d.ts CHANGED
@@ -71,6 +71,26 @@ type ScreenResult<A extends ScreenAction = ScreenAction> = {
71
71
  app?: unknown;
72
72
  };
73
73
 
74
+ declare const DEFAULT_BASE_PATH = "/clearly-hub";
75
+ declare const DEFAULT_HUB_URL = "https://hub.clearly.app";
76
+ declare const DEFAULT_AUTH_URL = "https://auth.clearly.app";
77
+ declare const DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
78
+ declare const ROUTES: {
79
+ readonly signIn: "/sign-in";
80
+ readonly callback: "/callback";
81
+ readonly signOut: "/sign-out";
82
+ readonly session: "/session";
83
+ readonly open: "/open";
84
+ readonly answer: "/answer";
85
+ readonly result: "/result";
86
+ readonly token: "/token";
87
+ };
88
+ declare const ANSWER_CHANNEL = "clearly-hub-answers";
89
+ interface AnswerMessage {
90
+ ref: string;
91
+ result: ScreenResult;
92
+ }
93
+
74
94
  interface SessionUser {
75
95
  sub: string;
76
96
  email: string;
@@ -89,24 +109,9 @@ type Session = {
89
109
  signedIn: true;
90
110
  user: SessionUser;
91
111
  };
92
-
93
- declare const DEFAULT_BASE_PATH = "/clearly-hub";
94
- declare const DEFAULT_HUB_URL = "https://hub.clearly.app";
95
- declare const DEFAULT_AUTH_URL = "https://auth.clearly.app";
96
- declare const DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
97
- declare const ROUTES: {
98
- readonly signIn: "/sign-in";
99
- readonly callback: "/callback";
100
- readonly signOut: "/sign-out";
101
- readonly session: "/session";
102
- readonly open: "/open";
103
- readonly answer: "/answer";
104
- readonly result: "/result";
105
- };
106
- declare const ANSWER_CHANNEL = "clearly-hub-answers";
107
- interface AnswerMessage {
108
- ref: string;
109
- result: ScreenResult;
112
+ interface AccessToken {
113
+ accessToken: string;
114
+ expiresIn: number;
110
115
  }
111
116
 
112
117
  interface SignedInPerson {
@@ -179,4 +184,4 @@ declare function clearlyHub<Req = unknown>(options: ClearlyHubOptions<Req>): {
179
184
  type ClearlyHub = ReturnType<typeof clearlyHub>;
180
185
 
181
186
  export { ANSWER_CHANNEL, DEFAULT_AUTH_URL, DEFAULT_BASE_PATH, DEFAULT_HUB_URL, DEFAULT_ISSUER, ROUTES, SCREEN_ACTIONS, checkScreenData, clearlyHub, isScreenAction };
182
- export type { AnswerContext, AnswerMessage, AppSubscription, ClearlyHub, ClearlyHubOptions, ClearlyHubUrls, CookieSource, HookContext, HubRef, HubSubscription, PickedResource, ScreenAction, ScreenAnswers, ScreenData, ScreenDisplay, ScreenResult, Session, SessionUser, SignInDecision, SignedInPerson };
187
+ export type { AccessToken, AnswerContext, AnswerMessage, AppSubscription, ClearlyHub, ClearlyHubOptions, ClearlyHubUrls, CookieSource, HookContext, HubRef, HubSubscription, PickedResource, ScreenAction, ScreenAnswers, ScreenData, ScreenDisplay, ScreenResult, Session, SessionUser, SignInDecision, SignedInPerson };
package/dist/index.js CHANGED
@@ -1,3 +1,20 @@
1
+ // ../shared/src/protocol.ts
2
+ var DEFAULT_BASE_PATH = "/clearly-hub";
3
+ var DEFAULT_HUB_URL = "https://hub.clearly.app";
4
+ var DEFAULT_AUTH_URL = "https://auth.clearly.app";
5
+ var DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
6
+ var ROUTES = {
7
+ signIn: "/sign-in",
8
+ callback: "/callback",
9
+ signOut: "/sign-out",
10
+ session: "/session",
11
+ open: "/open",
12
+ answer: "/answer",
13
+ result: "/result",
14
+ token: "/token"
15
+ };
16
+ var ANSWER_CHANNEL = "clearly-hub-answers";
17
+
1
18
  // ../shared/src/screens.ts
2
19
  var SCREEN_ACTIONS = [
3
20
  "SELECT_SUBSCRIPTION",
@@ -26,22 +43,6 @@ function checkScreenData(action, data) {
26
43
  return void 0;
27
44
  }
28
45
 
29
- // ../shared/src/protocol.ts
30
- var DEFAULT_BASE_PATH = "/clearly-hub";
31
- var DEFAULT_HUB_URL = "https://hub.clearly.app";
32
- var DEFAULT_AUTH_URL = "https://auth.clearly.app";
33
- var DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
34
- var ROUTES = {
35
- signIn: "/sign-in",
36
- callback: "/callback",
37
- signOut: "/sign-out",
38
- session: "/session",
39
- open: "/open",
40
- answer: "/answer",
41
- result: "/result"
42
- };
43
- var ANSWER_CHANNEL = "clearly-hub-answers";
44
-
45
46
  // src/config.ts
46
47
  var trimSlash = (url) => url.replace(/\/+$/, "");
47
48
  function resolveConfig(options) {
@@ -84,11 +85,15 @@ function toNodeMiddleware(handle, appUrl) {
84
85
  try {
85
86
  const headers = new Headers();
86
87
  for (const [name, value] of Object.entries(req.headers)) {
87
- if (Array.isArray(value)) value.forEach((item) => headers.append(name, item));
88
+ if (Array.isArray(value))
89
+ value.forEach((item) => headers.append(name, item));
88
90
  else if (value !== void 0) headers.set(name, value);
89
91
  }
90
92
  const url = new URL(req.originalUrl ?? req.url ?? "/", appUrl);
91
- const response = await handle(new Request(url, { method: req.method, headers }), req);
93
+ const response = await handle(
94
+ new Request(url, { method: req.method, headers }),
95
+ req
96
+ );
92
97
  if (response.headers.get("X-Clearly-Hub") === "unhandled" && next) {
93
98
  next();
94
99
  return;
@@ -142,16 +147,24 @@ var Screens = class {
142
147
  base64Decode(signature),
143
148
  new TextEncoder().encode(data)
144
149
  );
145
- if (!isSigned) throw new AnswerError("This answer was not signed by Clearly.HUB");
146
- const { request, response } = JSON.parse(new TextDecoder().decode(base64Decode(data)));
150
+ if (!isSigned)
151
+ throw new AnswerError("This answer was not signed by Clearly.HUB");
152
+ const { request, response } = JSON.parse(
153
+ new TextDecoder().decode(base64Decode(data))
154
+ );
147
155
  return { state: request.state, response };
148
156
  }
149
157
  // Fetched once; a key change needs a restart
150
158
  getPublicKey() {
151
159
  this.publicKey ??= this.config.fetch(this.config.urls.publicKey).then((response) => {
152
- if (!response.ok) throw new Error(`No public key at ${this.config.urls.publicKey}`);
160
+ if (!response.ok)
161
+ throw new Error(`No public key at ${this.config.urls.publicKey}`);
153
162
  return response.text();
154
- }).then((pem) => crypto.subtle.importKey("spki", pemToDer(pem), "Ed25519", false, ["verify"])).catch((error) => {
163
+ }).then(
164
+ (pem) => crypto.subtle.importKey("spki", pemToDer(pem), "Ed25519", false, [
165
+ "verify"
166
+ ])
167
+ ).catch((error) => {
155
168
  this.publicKey = void 0;
156
169
  throw error;
157
170
  });
@@ -162,7 +175,8 @@ var AnswerError = class extends Error {
162
175
  };
163
176
  function toScreenResult(action, response) {
164
177
  const { result, ...answer } = response;
165
- if (result === "SUCCESS") return { action, result: "SUCCESS", answer };
178
+ if (result === "SUCCESS")
179
+ return { action, result: "SUCCESS", answer };
166
180
  return { action, result: result === "FAILURE" ? "FAILURE" : "CANCELLED" };
167
181
  }
168
182
  function base64Encode(text) {
@@ -178,7 +192,11 @@ function readRawQuery(url) {
178
192
  const params = /* @__PURE__ */ new Map();
179
193
  for (const pair of url.search.slice(1).split("&")) {
180
194
  const index = pair.indexOf("=");
181
- if (index > 0) params.set(pair.slice(0, index), decodeURIComponent(pair.slice(index + 1)));
195
+ if (index > 0)
196
+ params.set(
197
+ pair.slice(0, index),
198
+ decodeURIComponent(pair.slice(index + 1))
199
+ );
182
200
  }
183
201
  return params;
184
202
  }
@@ -192,7 +210,8 @@ function createHandler(parts) {
192
210
  const url = new URL(request.url);
193
211
  if (!url.pathname.startsWith(config.basePath)) return notFound();
194
212
  const route = url.pathname.slice(config.basePath.length) || "/";
195
- if (request.method !== "GET") return new Response("Method not allowed", { status: 405 });
213
+ if (request.method !== "GET")
214
+ return new Response("Method not allowed", { status: 405 });
196
215
  const host = request.headers.get("host");
197
216
  const appHost = new URL(config.appUrl).host;
198
217
  if (host && host !== appHost && (route === ROUTES.signIn || route === ROUTES.open)) {
@@ -205,7 +224,8 @@ function createHandler(parts) {
205
224
  response = await routeRequest(route, url, data, parts, { request, req });
206
225
  } catch (error) {
207
226
  const isRefusedAnswer = error instanceof AnswerError;
208
- if (!isRefusedAnswer) console.error(`Clearly.HUB: ${route} failed:`, error);
227
+ if (!isRefusedAnswer)
228
+ console.error(`Clearly.HUB: ${route} failed:`, error);
209
229
  response = errorPage(isRefusedAnswer ? 400 : 500, describeError(error));
210
230
  }
211
231
  if (JSON.stringify(data) !== before) {
@@ -223,7 +243,8 @@ async function routeRequest(route, url, session, parts, incoming) {
223
243
  case ROUTES.callback:
224
244
  return finishSignIn(url, session, parts, incoming);
225
245
  case ROUTES.signOut:
226
- for (const key of Object.keys(session)) delete session[key];
246
+ for (const key of Object.keys(session))
247
+ delete session[key];
227
248
  return redirect(parts.signIn.signOutUrl());
228
249
  case ROUTES.session:
229
250
  return json(await readSession(session, parts));
@@ -236,12 +257,16 @@ async function routeRequest(route, url, session, parts, incoming) {
236
257
  delete session.result;
237
258
  return json(result);
238
259
  }
260
+ case ROUTES.token:
261
+ return readToken(session, parts);
239
262
  default:
240
263
  return notFound();
241
264
  }
242
265
  }
243
266
  async function startSignIn(url, session, { config, signIn }) {
244
- const { url: authorizationUrl, pending } = await signIn.start(safeReturnTo(url, config));
267
+ const { url: authorizationUrl, pending } = await signIn.start(
268
+ safeReturnTo(url, config)
269
+ );
245
270
  session.signIn = pending;
246
271
  return redirect(authorizationUrl);
247
272
  }
@@ -255,13 +280,28 @@ async function finishSignIn(url, session, { config, signIn }, incoming) {
255
280
  const { appUserId, redirectTo } = typeof decision === "string" ? { appUserId: decision, redirectTo: void 0 } : decision ?? {};
256
281
  session.user = { ...person, appUserId };
257
282
  session.tokens = tokens;
258
- return redirect(`${config.appUrl}${safePath(redirectTo, config) ?? pending.returnTo}`);
283
+ return redirect(
284
+ `${config.appUrl}${safePath(redirectTo, config) ?? pending.returnTo}`
285
+ );
259
286
  }
260
287
  async function readSession(session, parts) {
261
288
  const accessToken = await getFreshAccessToken(session, parts);
262
289
  if (!session.user || !accessToken) return { signedIn: false };
263
290
  return { signedIn: true, user: session.user };
264
291
  }
292
+ async function readToken(session, parts) {
293
+ const accessToken = await getFreshAccessToken(session, parts);
294
+ if (!session.user || !accessToken)
295
+ return json({ error: "Nobody is signed in with Clearly.HUB" }, 401);
296
+ const token = {
297
+ accessToken,
298
+ expiresIn: Math.max(
299
+ 0,
300
+ session.tokens.expiresAt - Math.floor(Date.now() / 1e3)
301
+ )
302
+ };
303
+ return json(token);
304
+ }
265
305
  async function getFreshAccessToken(session, { signIn }) {
266
306
  const tokens = session.tokens;
267
307
  if (!tokens) return void 0;
@@ -278,7 +318,8 @@ async function getFreshAccessToken(session, { signIn }) {
278
318
  }
279
319
  function openScreen(url, session, { config, screens }) {
280
320
  const action = url.searchParams.get("action");
281
- if (!isScreenAction(action)) return errorPage(400, `Unknown screen: ${action}`);
321
+ if (!isScreenAction(action))
322
+ return errorPage(400, `Unknown screen: ${action}`);
282
323
  const display = url.searchParams.get("display") === "redirect" ? "redirect" : "popup";
283
324
  const dataParam = url.searchParams.get("data");
284
325
  let data;
@@ -309,7 +350,9 @@ async function receiveAnswer(url, session, { config, screens }, incoming) {
309
350
  );
310
351
  const pending = state ? session.screens?.[state] : void 0;
311
352
  if (!pending || Date.now() - pending.createdAt > SCREEN_TTL_MS) {
312
- throw new AnswerError("This answer does not belong to a screen opened in this browser");
353
+ throw new AnswerError(
354
+ "This answer does not belong to a screen opened in this browser"
355
+ );
313
356
  }
314
357
  delete session.screens[state];
315
358
  let result = toScreenResult(pending.action, response);
@@ -321,7 +364,11 @@ async function receiveAnswer(url, session, { config, screens }, incoming) {
321
364
  });
322
365
  if (app !== void 0) result = { ...result, app };
323
366
  } catch (error) {
324
- result = { action: pending.action, result: "FAILURE", error: error.message };
367
+ result = {
368
+ action: pending.action,
369
+ result: "FAILURE",
370
+ error: error.message
371
+ };
325
372
  }
326
373
  if (pending.display === "redirect") {
327
374
  session.result = { ref: pending.ref, result };
@@ -356,11 +403,21 @@ function safePath(path, config) {
356
403
  }
357
404
  }
358
405
  var redirect = (location) => new Response(null, { status: 302, headers: { Location: location } });
359
- var json = (body) => new Response(JSON.stringify(body), {
360
- headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
406
+ var json = (body, status = 200) => new Response(JSON.stringify(body), {
407
+ status,
408
+ headers: {
409
+ "Content-Type": "application/json",
410
+ "Cache-Control": "no-store"
411
+ }
412
+ });
413
+ var html = (body, status = 200) => new Response(body, {
414
+ status,
415
+ headers: { "Content-Type": "text/html; charset=utf-8" }
416
+ });
417
+ var notFound = () => new Response("Not found", {
418
+ status: 404,
419
+ headers: { "X-Clearly-Hub": "unhandled" }
361
420
  });
362
- var html = (body, status = 200) => new Response(body, { status, headers: { "Content-Type": "text/html; charset=utf-8" } });
363
- var notFound = () => new Response("Not found", { status: 404, headers: { "X-Clearly-Hub": "unhandled" } });
364
421
  function describeError(error) {
365
422
  const messages = [];
366
423
  for (let current = error; current instanceof Error && messages.length < 5; current = current.cause) {
@@ -369,7 +426,10 @@ function describeError(error) {
369
426
  return messages.join(": ") || String(error);
370
427
  }
371
428
  function errorPage(status, message) {
372
- const escaped = message.replace(/[&<>"]/g, (char) => `&#${char.charCodeAt(0)};`);
429
+ const escaped = message.replace(
430
+ /[&<>"]/g,
431
+ (char) => `&#${char.charCodeAt(0)};`
432
+ );
373
433
  return html(
374
434
  `<!doctype html><meta charset="utf-8"><title>Clearly.HUB</title><p>${escaped}</p>`,
375
435
  status
@@ -391,7 +451,9 @@ var HubApi = class {
391
451
  try {
392
452
  return await this.config.fetch(url, init);
393
453
  } catch (error) {
394
- throw new Error(`Clearly.HUB could not be reached at ${url}`, { cause: error });
454
+ throw new Error(`Clearly.HUB could not be reached at ${url}`, {
455
+ cause: error
456
+ });
395
457
  }
396
458
  }
397
459
  get hasAppKey() {
@@ -401,11 +463,16 @@ var HubApi = class {
401
463
  // until shortly before it expires
402
464
  async getAppToken() {
403
465
  const now = Date.now() / 1e3;
404
- if (this.appToken && this.appToken.expiresAt > now + 60) return this.appToken.value;
466
+ if (this.appToken && this.appToken.expiresAt > now + 60)
467
+ return this.appToken.value;
405
468
  if (!this.hasAppKey) {
406
- throw new Error("Clearly.HUB: m2mClientId and m2mClientSecret are needed for this");
469
+ throw new Error(
470
+ "Clearly.HUB: m2mClientId and m2mClientSecret are needed for this"
471
+ );
407
472
  }
408
- const credentials = btoa(`${this.config.m2mClientId}:${this.config.m2mClientSecret}`);
473
+ const credentials = btoa(
474
+ `${this.config.m2mClientId}:${this.config.m2mClientSecret}`
475
+ );
409
476
  const response = await this.call(`${this.config.urls.auth}/oauth2/token`, {
410
477
  method: "POST",
411
478
  headers: {
@@ -421,7 +488,10 @@ var HubApi = class {
421
488
  throw new Error(`Clearly.HUB refused the app's key (${response.status})`);
422
489
  }
423
490
  const body = await response.json();
424
- this.appToken = { value: body.access_token, expiresAt: now + body.expires_in };
491
+ this.appToken = {
492
+ value: body.access_token,
493
+ expiresAt: now + body.expires_in
494
+ };
425
495
  return body.access_token;
426
496
  }
427
497
  // Every hub that bought your app, with status and your plan id
@@ -429,11 +499,16 @@ var HubApi = class {
429
499
  if (this.subscriptions && Date.now() - this.subscriptions.fetchedAt < SUBSCRIPTIONS_CACHE_MS) {
430
500
  return this.subscriptions.value;
431
501
  }
432
- const response = await this.call(`${this.config.urls.api}/api/app-api/subscriptions`, {
433
- headers: { Authorization: `Bearer ${await this.getAppToken()}` }
434
- });
502
+ const response = await this.call(
503
+ `${this.config.urls.api}/api/app-api/subscriptions`,
504
+ {
505
+ headers: { Authorization: `Bearer ${await this.getAppToken()}` }
506
+ }
507
+ );
435
508
  if (!response.ok) {
436
- throw new Error(`Could not read the subscriptions from Clearly.HUB (${response.status})`);
509
+ throw new Error(
510
+ `Could not read the subscriptions from Clearly.HUB (${response.status})`
511
+ );
437
512
  }
438
513
  const items = await response.json();
439
514
  const value = items.map((item) => ({
@@ -466,12 +541,16 @@ var HubApi = class {
466
541
  });
467
542
  if (!response.ok) {
468
543
  const body = await response.json().catch(() => ({}));
469
- throw new Error(`Clearly.HUB did not accept the usage: ${body.message ?? response.status}`);
544
+ throw new Error(
545
+ `Clearly.HUB did not accept the usage: ${body.message ?? response.status}`
546
+ );
470
547
  }
471
548
  }
472
549
  };
473
550
  function pickSubscription(subscriptions, hubId) {
474
- const ofHub = subscriptions.filter((subscription) => subscription.hubId === hubId);
551
+ const ofHub = subscriptions.filter(
552
+ (subscription) => subscription.hubId === hubId
553
+ );
475
554
  return ofHub.find((s) => s.status === "ACTIVE") ?? ofHub.find((s) => s.status === "PENDING");
476
555
  }
477
556
 
@@ -488,7 +567,12 @@ var SignIn = class {
488
567
  }
489
568
  // Discovery happens once, on the first sign-in
490
569
  getConfiguration() {
491
- this.configuration ??= oidc.discovery(new URL(this.config.urls.issuer), this.config.clientId, void 0, oidc.None()).catch((error) => {
570
+ this.configuration ??= oidc.discovery(
571
+ new URL(this.config.urls.issuer),
572
+ this.config.clientId,
573
+ void 0,
574
+ oidc.None()
575
+ ).catch((error) => {
492
576
  this.configuration = void 0;
493
577
  throw error;
494
578
  });
@@ -510,11 +594,15 @@ var SignIn = class {
510
594
  return { url: url.href, pending: { codeVerifier, state, nonce, returnTo } };
511
595
  }
512
596
  async finish(currentUrl, pending) {
513
- const tokens = await oidc.authorizationCodeGrant(await this.getConfiguration(), currentUrl, {
514
- pkceCodeVerifier: pending.codeVerifier,
515
- expectedState: pending.state,
516
- expectedNonce: pending.nonce
517
- });
597
+ const tokens = await oidc.authorizationCodeGrant(
598
+ await this.getConfiguration(),
599
+ currentUrl,
600
+ {
601
+ pkceCodeVerifier: pending.codeVerifier,
602
+ expectedState: pending.state,
603
+ expectedNonce: pending.nonce
604
+ }
605
+ );
518
606
  const claims = tokens.claims();
519
607
  if (!claims) throw new Error("Clearly.HUB sent no ID token");
520
608
  return {
@@ -527,7 +615,10 @@ var SignIn = class {
527
615
  };
528
616
  }
529
617
  async refresh(refreshToken) {
530
- const tokens = await oidc.refreshTokenGrant(await this.getConfiguration(), refreshToken);
618
+ const tokens = await oidc.refreshTokenGrant(
619
+ await this.getConfiguration(),
620
+ refreshToken
621
+ );
531
622
  return toSessionTokens(tokens, refreshToken);
532
623
  }
533
624
  // Signing out of Clearly.HUB too, or the next sign-in goes straight through
@@ -560,12 +651,16 @@ function parseCookies(header) {
560
651
  const cookies = /* @__PURE__ */ new Map();
561
652
  for (const part of header.split(";")) {
562
653
  const index = part.indexOf("=");
563
- if (index > 0) cookies.set(part.slice(0, index).trim(), part.slice(index + 1).trim());
654
+ if (index > 0)
655
+ cookies.set(part.slice(0, index).trim(), part.slice(index + 1).trim());
564
656
  }
565
657
  return cookies;
566
658
  }
567
659
  async function deriveKey(secret) {
568
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
660
+ const digest = await crypto.subtle.digest(
661
+ "SHA-256",
662
+ new TextEncoder().encode(secret)
663
+ );
569
664
  return new Uint8Array(digest);
570
665
  }
571
666
  var SessionStore = class {
@@ -583,8 +678,14 @@ var SessionStore = class {
583
678
  }
584
679
  if (!chunks.length) return { data: {}, chunkCount: 0 };
585
680
  try {
586
- const { plaintext } = await compactDecrypt(chunks.join(""), await this.keyPromise);
587
- return { data: JSON.parse(new TextDecoder().decode(plaintext)), chunkCount: chunks.length };
681
+ const { plaintext } = await compactDecrypt(
682
+ chunks.join(""),
683
+ await this.keyPromise
684
+ );
685
+ return {
686
+ data: JSON.parse(new TextDecoder().decode(plaintext)),
687
+ chunkCount: chunks.length
688
+ };
588
689
  } catch {
589
690
  return { data: {}, chunkCount: chunks.length };
590
691
  }
@@ -592,7 +693,9 @@ var SessionStore = class {
592
693
  // Set-Cookie headers for the new session, clearing chunks it no longer needs
593
694
  async write(data, previousChunkCount) {
594
695
  const isEmpty = Object.keys(data).length === 0;
595
- const chunks = isEmpty ? [] : (await this.encrypt(data)).match(new RegExp(`.{1,${CHUNK_SIZE}}`, "g"));
696
+ const chunks = isEmpty ? [] : (await this.encrypt(data)).match(
697
+ new RegExp(`.{1,${CHUNK_SIZE}}`, "g")
698
+ );
596
699
  const attributes = `Path=/; HttpOnly; SameSite=Lax${this.config.isSecure ? "; Secure" : ""}`;
597
700
  const headers = chunks.map(
598
701
  (chunk, i) => `${COOKIE_NAME}.${i}=${chunk}; Max-Age=${MAX_AGE_SECONDS}; ${attributes}`
@@ -642,7 +745,8 @@ function clearlyHub(options) {
642
745
  // person's key. Open data works without it.
643
746
  async fetchAsUser(request, url, init = {}) {
644
747
  const token = await getUserToken(request);
645
- if (!token) throw new Error("Clearly.HUB: nobody is signed in with Clearly.HUB");
748
+ if (!token)
749
+ throw new Error("Clearly.HUB: nobody is signed in with Clearly.HUB");
646
750
  const headers = new Headers(init.headers);
647
751
  headers.set("Authorization", token);
648
752
  return config.fetch(url, { ...init, headers });
@@ -667,11 +771,18 @@ function clearlyHub(options) {
667
771
  }) {
668
772
  const person = sub ?? (request && (await parts.sessions.read(request)).data.user?.sub);
669
773
  if (!person) {
670
- throw new Error("Clearly.HUB: reportUsage needs the sub of a Clearly.HUB account");
774
+ throw new Error(
775
+ "Clearly.HUB: reportUsage needs the sub of a Clearly.HUB account"
776
+ );
671
777
  }
672
- const subscription = pickSubscription(await parts.api.getSubscriptions(), hubId);
778
+ const subscription = pickSubscription(
779
+ await parts.api.getSubscriptions(),
780
+ hubId
781
+ );
673
782
  if (subscription?.status !== "ACTIVE") {
674
- throw new Error(`Clearly.HUB: hub ${hubId} has no active subscription to your app`);
783
+ throw new Error(
784
+ `Clearly.HUB: hub ${hubId} has no active subscription to your app`
785
+ );
675
786
  }
676
787
  await parts.api.reportUsage({
677
788
  sub: person,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clearly-hub/node",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Clearly.HUB for Node servers: sign-in, hosted screens, subscriptions and usage in one handler",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Future Insight Group BV",