@ghub/identity-chrome 0.0.0-stage → 0.2.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 +115 -0
- package/README.md +157 -2
- package/dist/catalogue.d.ts +39 -0
- package/dist/catalogue.js +79 -0
- package/dist/core.d.ts +69 -0
- package/dist/core.js +248 -0
- package/dist/elements/account.d.ts +96 -0
- package/dist/elements/account.js +1008 -0
- package/dist/elements/apps.d.ts +67 -0
- package/dist/elements/apps.js +689 -0
- package/dist/elements/contract.d.ts +29 -0
- package/dist/elements/contract.js +109 -0
- package/dist/elements/dom.d.ts +13 -0
- package/dist/elements/dom.js +45 -0
- package/dist/elements/index.d.ts +48 -0
- package/dist/elements/index.js +26 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +35 -0
- package/dist/react.d.ts +41 -0
- package/dist/react.js +73 -0
- package/dist/server.d.ts +65 -0
- package/dist/server.js +148 -0
- package/dist/tint.d.ts +22 -0
- package/dist/tint.js +41 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +9 -0
- package/dist/validate.d.ts +22 -0
- package/dist/validate.js +148 -0
- package/package.json +61 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Copyright (c) 2026 Gelhaus Solutions (Enno Gelhaus). All rights reserved.
|
|
2
|
+
|
|
3
|
+
Elastic License 2.0 (ELv2)
|
|
4
|
+
|
|
5
|
+
URL: https://www.elastic.co/licensing/elastic-license
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
|
14
|
+
non-sublicensable, non-transferable license to use, copy, distribute, make
|
|
15
|
+
available, and prepare derivative works of the software, in each case subject to
|
|
16
|
+
the limitations and conditions below.
|
|
17
|
+
|
|
18
|
+
## Limitations
|
|
19
|
+
|
|
20
|
+
You may not provide the software to third parties as a hosted or managed
|
|
21
|
+
service, where the service provides users with access to any substantial set of
|
|
22
|
+
the features or functionality of the software.
|
|
23
|
+
|
|
24
|
+
You may not move, change, disable, or circumvent the license key functionality
|
|
25
|
+
in the software, and you may not remove or obscure any functionality in the
|
|
26
|
+
software that is protected by the license key.
|
|
27
|
+
|
|
28
|
+
You may not alter, remove, or obscure any licensing, copyright, or other notices
|
|
29
|
+
of the licensor in the software. Any use of the licensor's trademarks is subject
|
|
30
|
+
to applicable law.
|
|
31
|
+
|
|
32
|
+
## Patents
|
|
33
|
+
|
|
34
|
+
The licensor grants you a license, under any patent claims the licensor can
|
|
35
|
+
license, or becomes able to license, to make, have made, use, sell, offer for
|
|
36
|
+
sale, import and have imported the software, in each case subject to the
|
|
37
|
+
limitations and conditions in this license. This license does not cover any
|
|
38
|
+
patent claims that you cause to be infringed by modifications or additions to
|
|
39
|
+
the software. If you or your company make any written claim that the software
|
|
40
|
+
infringes or contributes to infringement of any patent, your patent license for
|
|
41
|
+
the software granted under these terms ends immediately. If your company makes
|
|
42
|
+
such a claim, your patent license ends immediately for work on behalf of your
|
|
43
|
+
company.
|
|
44
|
+
|
|
45
|
+
## Notices
|
|
46
|
+
|
|
47
|
+
You must ensure that anyone who gets a copy of any part of the software from you
|
|
48
|
+
also gets a copy of these terms.
|
|
49
|
+
|
|
50
|
+
If you modify the software, you must include in any modified copies of the
|
|
51
|
+
software prominent notices stating that you have modified the software.
|
|
52
|
+
|
|
53
|
+
## No Other Rights
|
|
54
|
+
|
|
55
|
+
These terms do not imply any licenses other than those expressly granted in
|
|
56
|
+
these terms.
|
|
57
|
+
|
|
58
|
+
## Termination
|
|
59
|
+
|
|
60
|
+
If you use the software in violation of these terms, such use is not licensed,
|
|
61
|
+
and your licenses will automatically terminate. If the licensor provides you
|
|
62
|
+
with a notice of your violation, and you cease all violation of this license no
|
|
63
|
+
later than 30 days after you receive that notice, your licenses will be
|
|
64
|
+
reinstated retroactively. However, if you violate these terms after such
|
|
65
|
+
reinstatement, any additional violation of these terms will cause your licenses
|
|
66
|
+
to terminate automatically and permanently.
|
|
67
|
+
|
|
68
|
+
## No Liability
|
|
69
|
+
|
|
70
|
+
*As far as the law allows, the software comes as is, without any warranty or
|
|
71
|
+
condition, and the licensor will not be liable to you for any damages arising
|
|
72
|
+
out of these terms or the use or nature of the software, under any kind of legal
|
|
73
|
+
claim.*
|
|
74
|
+
|
|
75
|
+
## Definitions
|
|
76
|
+
|
|
77
|
+
The **licensor** is the entity offering these terms, and the **software** is the
|
|
78
|
+
software the licensor makes available under these terms, including any portion
|
|
79
|
+
of it.
|
|
80
|
+
|
|
81
|
+
**you** refers to the individual or entity agreeing to these terms.
|
|
82
|
+
|
|
83
|
+
**your company** is any legal entity, sole proprietorship, or other kind of
|
|
84
|
+
organization that you work for, plus all organizations that have control over,
|
|
85
|
+
are under the control of, or are under common control with that organization.
|
|
86
|
+
**control** means ownership of substantially all the assets of an entity, or the
|
|
87
|
+
power to direct its management and policies by vote, contract, or otherwise.
|
|
88
|
+
Control can be direct or indirect.
|
|
89
|
+
|
|
90
|
+
**your licenses** are all the licenses granted to you for the software under
|
|
91
|
+
these terms.
|
|
92
|
+
|
|
93
|
+
**use** means anything you do with the software requiring one of your licenses.
|
|
94
|
+
|
|
95
|
+
**trademark** means trademarks, service marks, and similar rights.
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Additional restriction
|
|
99
|
+
|
|
100
|
+
In addition to the limitations in the Elastic License 2.0 above, and as a
|
|
101
|
+
condition of the licence granted by it:
|
|
102
|
+
|
|
103
|
+
You may not use the software for commercial purposes without a separate
|
|
104
|
+
business licence granted in writing by Gelhaus Solutions.
|
|
105
|
+
|
|
106
|
+
Commercial purposes include use in, by or for a business or other commercial
|
|
107
|
+
undertaking, whether or not the software is provided to anyone outside it, and
|
|
108
|
+
whether or not a fee is charged. Non-commercial use is not restricted by this
|
|
109
|
+
paragraph.
|
|
110
|
+
|
|
111
|
+
This restriction is ours and is not part of the Elastic License 2.0. The
|
|
112
|
+
Elastic License 2.0 by itself permits commercial internal use; this paragraph
|
|
113
|
+
removes that permission. Where the two conflict, this paragraph applies.
|
|
114
|
+
|
|
115
|
+
For a business licence, contact egelhaus@ennogelhaus.de.
|
package/README.md
CHANGED
|
@@ -1,3 +1,158 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ghub/identity-chrome
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The shared header controls of the Gelhaus Solutions products: the apps picker and the
|
|
4
|
+
account popup. Two custom elements with shadow DOM (`<gp-apps>`, `<gp-account>`) and React
|
|
5
|
+
wrappers for them; the browser half under both (what the controls know, what they keep, what
|
|
6
|
+
they do when Identity cannot be reached); and the server half (a product's own server asking
|
|
7
|
+
GPlatform Identity).
|
|
8
|
+
|
|
9
|
+
Nothing here depends on a product signing in through Identity. A product that has not
|
|
10
|
+
adopted it uses the same package the same way.
|
|
11
|
+
|
|
12
|
+
## Off unless enabled
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createChromeCore } from "@ghub/identity-chrome";
|
|
16
|
+
|
|
17
|
+
const chrome = createChromeCore({
|
|
18
|
+
enabled: true, // anything but exactly true makes no request and reads no storage
|
|
19
|
+
endpoint: "/api/identity-chrome", // the product's own route, same origin
|
|
20
|
+
storage: window.localStorage, // optional: the last list, held for an outage
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
chrome.subscribe((state) => draw(state));
|
|
24
|
+
await chrome.load();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A self-hosted or customer-run installation never sets `enabled`, so it never calls anybody.
|
|
28
|
+
The last answer is drawn at once while a fresh one is fetched, is kept and marked `stale`
|
|
29
|
+
when the refresh fails, and is thirty days old at most. Call `chrome.clear()` on sign-out, so
|
|
30
|
+
a shared device never shows the next person the previous one's products.
|
|
31
|
+
|
|
32
|
+
Every address in an answer is checked before it is drawn: a tile or an icon whose address is
|
|
33
|
+
not `http` or `https` is dropped, never repaired, in a fresh answer and in the cached copy
|
|
34
|
+
alike.
|
|
35
|
+
|
|
36
|
+
## The product's own route
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { createChromeServer } from "@ghub/identity-chrome/server";
|
|
40
|
+
|
|
41
|
+
const chrome = createChromeServer({
|
|
42
|
+
baseUrl: process.env.GPID_URL,
|
|
43
|
+
product: "advisory",
|
|
44
|
+
token: process.env.GPID_CHROME_TOKEN, // never sent to a browser
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Any framework that speaks the web Request and Response:
|
|
48
|
+
export const GET = chrome.handler(async (request) => {
|
|
49
|
+
const user = await sessionOf(request);
|
|
50
|
+
return user === null ? null : { email: user.verifiedEmail };
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The route says only who is signed in, by an address the product has verified, and Identity
|
|
55
|
+
answers with the products that person has and the public ones they do not, and the two
|
|
56
|
+
footer links of the account popup (the terms and the privacy policy, the same in every
|
|
57
|
+
product, so a moved address needs no new release here). An unverified
|
|
58
|
+
address is a way to ask about somebody else. Answers are reused for a minute, and the token
|
|
59
|
+
is never in anything this package throws or answers.
|
|
60
|
+
|
|
61
|
+
A one-off push of a product's users (`chrome.seed(emails)`) is refused unless staff have
|
|
62
|
+
opened a window for it on Identity's product screen, and `chrome.remove(emails)` says people
|
|
63
|
+
no longer have access.
|
|
64
|
+
|
|
65
|
+
## The two controls
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { createChromeCore, defineChromeElements } from "@ghub/identity-chrome";
|
|
69
|
+
|
|
70
|
+
defineChromeElements(); // <gp-apps> and <gp-account>, each once; a no-op on a server
|
|
71
|
+
|
|
72
|
+
const apps = document.querySelector("gp-apps")!;
|
|
73
|
+
apps.core = chrome; // the core above: one fetch and one cache behind both
|
|
74
|
+
|
|
75
|
+
const account = document.querySelector("gp-account")!;
|
|
76
|
+
account.core = chrome;
|
|
77
|
+
account.product = "GAdvisory"; // your own name, from your catalogue entry
|
|
78
|
+
account.person = { name: user.name, email: user.email, id: user.id, avatarUrl: user.picture };
|
|
79
|
+
account.organisations = [{ id: "org_1", name: "Hafen Labs", role: "admin", active: true }];
|
|
80
|
+
account.onSwitch = async (id) => switchOrganisation(id); // reject when it did not switch
|
|
81
|
+
account.onSignOut = async () => {
|
|
82
|
+
await signOut(); // reject when it failed: the popup says they are still signed in
|
|
83
|
+
location.assign("/"); // you navigate; as this resolves the element forgets the kept list
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Everything is a property, because a core, a person and a callback are objects. Each can be
|
|
88
|
+
set before the element is defined. Neither control renders anything while the core is off,
|
|
89
|
+
and the account popup renders nothing without a person.
|
|
90
|
+
|
|
91
|
+
**The account popup shows mostly what the product passes**: the name and address from your
|
|
92
|
+
session, your organisations with their roles (Owner, Admin or Member) and your two actions.
|
|
93
|
+
Identity adds the picture, the colour of the initial, the links to its own account and set up
|
|
94
|
+
screens and the footer, so the popup still signs somebody out while Identity is down. The
|
|
95
|
+
picture is Identity's if the person has one, else yours, else a coloured initial whose tint
|
|
96
|
+
Identity picks from its own id; for somebody with no Identity, pass your own stable user id as
|
|
97
|
+
`id` and the same hash (`avatarTint`) picks it. Set `viaIdentity` once you sign in through
|
|
98
|
+
Identity, and Sign out says it ends both. Sign out leaves this product only.
|
|
99
|
+
|
|
100
|
+
Every word can be replaced through `labels`; the rest stay English.
|
|
101
|
+
|
|
102
|
+
### In React
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { GpAccount, GpApps } from "@ghub/identity-chrome/react";
|
|
106
|
+
|
|
107
|
+
<GpApps core={chrome} />
|
|
108
|
+
<GpAccount core={chrome} product="GAdvisory" person={person} organisations={orgs} onSwitch={switchTo} onSignOut={signOut} />
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The wrappers define the elements and set the properties after mounting, so they render on a
|
|
112
|
+
server as the bare tags. React 19 or newer, as an optional peer.
|
|
113
|
+
|
|
114
|
+
### The host contract
|
|
115
|
+
|
|
116
|
+
Shadow DOM: nothing reaches in and nothing leaks out but the custom properties a page
|
|
117
|
+
declares on its root, which inherit into the shadow. The controls read the published
|
|
118
|
+
`@ghub/tokens` names, so a page on `@ghub/tokens` is styled with no setup, and Aurora's frosted
|
|
119
|
+
menu where the page is on Aurora:
|
|
120
|
+
|
|
121
|
+
| Reads | For |
|
|
122
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
123
|
+
| `--ga-menu`, else `--gm-overlay`; `--ga-frost-menu`; `--ga-shadow-menu`, else `--gm-shadow-overlay` | the picker's surface, its frost and its shadow |
|
|
124
|
+
| `--ga-menu`, else `--gm-sunken` | the account popup's tinted ground |
|
|
125
|
+
| `--ga-track`, else `--gm-overlay` | the account popup's panels |
|
|
126
|
+
| `--gm-plate`, `--gm-ink`, `--gm-ink-2`, `--gm-ink-3`, `--gm-hairline`, `--gm-control`, `--gm-hover`, `--gm-active`, `--gm-selected`, `--gm-accent`, `--gm-accent-text`, `--gm-ring`, `--gm-crit-ink` | everything else |
|
|
127
|
+
| `--font-sans` | the face |
|
|
128
|
+
|
|
129
|
+
A page that declares none of them gets modern light, or modern dark under
|
|
130
|
+
`prefers-color-scheme: dark`. There is no second accent: the controls take the page's. Both
|
|
131
|
+
follow reduced motion, reduced transparency and forced colours, and below 600 wide each is a
|
|
132
|
+
modal bottom sheet. `part="trigger"` and `part="popup"` are there for placement only.
|
|
133
|
+
|
|
134
|
+
## A product's own name and mark
|
|
135
|
+
|
|
136
|
+
Identity's catalogue is where every product's name and mark live. Read your own from it for
|
|
137
|
+
your top bar, your title and your favicon, and a rename or a new mark reaches you without a
|
|
138
|
+
release:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { catalogueMarkUrl, createCatalogue } from "@ghub/identity-chrome";
|
|
142
|
+
|
|
143
|
+
const catalogue = createCatalogue({ baseUrl: process.env.GPID_URL });
|
|
144
|
+
const me = await catalogue.entry("gadvisory"); // { displayName, homeUrl, iconUrl, monoIconUrl, ... }
|
|
145
|
+
|
|
146
|
+
// Where you cannot ask first, an address that always leads to the current mark:
|
|
147
|
+
// <link rel="icon" type="image/svg+xml" href={catalogueMarkUrl(process.env.GPID_URL, "gadvisory")} />
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
No token is needed. An entry is reused for a minute, and after a failed ask the last good one
|
|
151
|
+
is returned, so an outage leaves the name and mark you last had. A self-hosted install asks
|
|
152
|
+
nobody and ships the copy its release was built with.
|
|
153
|
+
|
|
154
|
+
The shapes are kept in step by hand with `ChromeView` and `CatalogueEntry` in GPlatform
|
|
155
|
+
Identity, which this public package cannot depend on.
|
|
156
|
+
|
|
157
|
+
Part of [GHub-Packages](https://github.com/Gelhaus-Solutions/GHub-Packages), the packages
|
|
158
|
+
the Gelhaus Solutions products share.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A product's own name and marks, read from Identity's catalogue.
|
|
3
|
+
*
|
|
4
|
+
* The catalogue is where a product's name and mark live. A product reads its own
|
|
5
|
+
* here when it draws its top bar, its title and its favicon, so a rename or a new
|
|
6
|
+
* mark reaches every product without a release. Nothing here needs a token: a
|
|
7
|
+
* product's name and mark are on its own pages already.
|
|
8
|
+
*
|
|
9
|
+
* Two ways in. `catalogueMarkUrl` is an address that always leads to the current
|
|
10
|
+
* mark, for places that cannot ask first (a favicon link, an `<img>` in static
|
|
11
|
+
* markup). `createCatalogue` asks for the whole entry and keeps the last good
|
|
12
|
+
* answer, so an Identity outage leaves the name and mark a product last had
|
|
13
|
+
* rather than none.
|
|
14
|
+
*
|
|
15
|
+
* Off for a self-hosted install, as the controls are: such an install ships the
|
|
16
|
+
* copy its release was built with and asks nobody.
|
|
17
|
+
*/
|
|
18
|
+
import type { CatalogueEntry } from "./types.js";
|
|
19
|
+
/** The address that always leads to a product's current mark. Redirects, briefly cached. */
|
|
20
|
+
export declare function catalogueMarkUrl(baseUrl: string, slug: string, variant?: "colour" | "mono"): string;
|
|
21
|
+
export interface CatalogueOptions {
|
|
22
|
+
/** Identity's public address, with or without a trailing slash. */
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
fetch?: typeof fetch;
|
|
25
|
+
/** Milliseconds. A call that hangs must not hang a page render. */
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
/** How long an entry is reused before asking again, in seconds. Identity caches it for a minute too. */
|
|
28
|
+
cacheSeconds?: number;
|
|
29
|
+
now?: () => number;
|
|
30
|
+
}
|
|
31
|
+
export interface Catalogue {
|
|
32
|
+
/**
|
|
33
|
+
* A product's entry. After a failed ask, the last good answer, however old;
|
|
34
|
+
* null only when there never was one or the product does not exist.
|
|
35
|
+
*/
|
|
36
|
+
entry(slug: string): Promise<CatalogueEntry | null>;
|
|
37
|
+
}
|
|
38
|
+
export declare function createCatalogue(options: CatalogueOptions): Catalogue;
|
|
39
|
+
//# sourceMappingURL=catalogue.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A product's own name and marks, read from Identity's catalogue.
|
|
4
|
+
*
|
|
5
|
+
* The catalogue is where a product's name and mark live. A product reads its own
|
|
6
|
+
* here when it draws its top bar, its title and its favicon, so a rename or a new
|
|
7
|
+
* mark reaches every product without a release. Nothing here needs a token: a
|
|
8
|
+
* product's name and mark are on its own pages already.
|
|
9
|
+
*
|
|
10
|
+
* Two ways in. `catalogueMarkUrl` is an address that always leads to the current
|
|
11
|
+
* mark, for places that cannot ask first (a favicon link, an `<img>` in static
|
|
12
|
+
* markup). `createCatalogue` asks for the whole entry and keeps the last good
|
|
13
|
+
* answer, so an Identity outage leaves the name and mark a product last had
|
|
14
|
+
* rather than none.
|
|
15
|
+
*
|
|
16
|
+
* Off for a self-hosted install, as the controls are: such an install ships the
|
|
17
|
+
* copy its release was built with and asks nobody.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.catalogueMarkUrl = catalogueMarkUrl;
|
|
21
|
+
exports.createCatalogue = createCatalogue;
|
|
22
|
+
const validate_js_1 = require("./validate.js");
|
|
23
|
+
/** The address that always leads to a product's current mark. Redirects, briefly cached. */
|
|
24
|
+
function catalogueMarkUrl(baseUrl, slug, variant = "colour") {
|
|
25
|
+
const base = baseUrl.replace(/\/+$/u, "");
|
|
26
|
+
return `${base}/api/v1/catalogue/${encodeURIComponent(slug)}/icon${variant === "mono" ? "/mono" : ""}`;
|
|
27
|
+
}
|
|
28
|
+
const DEFAULT_TIMEOUT_MS = 3000;
|
|
29
|
+
const DEFAULT_CACHE_SECONDS = 60;
|
|
30
|
+
function createCatalogue(options) {
|
|
31
|
+
const base = options.baseUrl.replace(/\/+$/u, "");
|
|
32
|
+
const now = options.now ?? Date.now;
|
|
33
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
34
|
+
const ttlMs = (options.cacheSeconds ?? DEFAULT_CACHE_SECONDS) * 1000;
|
|
35
|
+
const held = new Map();
|
|
36
|
+
async function ask(slug) {
|
|
37
|
+
const send = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
controller.abort();
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
const response = await send(`${base}/api/v1/catalogue/${encodeURIComponent(slug)}`, {
|
|
44
|
+
method: "GET",
|
|
45
|
+
headers: { accept: "application/json" },
|
|
46
|
+
credentials: "omit",
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
});
|
|
49
|
+
if (response.status === 404)
|
|
50
|
+
return null;
|
|
51
|
+
if (!response.ok)
|
|
52
|
+
throw new Error(`Identity answered ${String(response.status)}`);
|
|
53
|
+
const entry = (0, validate_js_1.parseCatalogueEntry)(await response.json());
|
|
54
|
+
if (entry === null)
|
|
55
|
+
throw new Error("Identity's answer was not understood");
|
|
56
|
+
return entry;
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
async entry(slug) {
|
|
64
|
+
const last = held.get(slug);
|
|
65
|
+
if (last !== undefined && now() - last.at < ttlMs)
|
|
66
|
+
return last.entry;
|
|
67
|
+
try {
|
|
68
|
+
const entry = await ask(slug);
|
|
69
|
+
held.set(slug, { entry, at: now() });
|
|
70
|
+
return entry;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// The name and mark it last had, rather than none.
|
|
74
|
+
return last?.entry ?? null;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=catalogue.js.map
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser half: what the header controls know, and what they keep.
|
|
3
|
+
*
|
|
4
|
+
* It owns no markup. The apps picker and the account popup draw from `state()`
|
|
5
|
+
* and ask it to `load()`, which is what keeps one fetch, one cache and one
|
|
6
|
+
* failure story behind both controls.
|
|
7
|
+
*
|
|
8
|
+
* **Off unless enabled, and off means silent.** Nothing is fetched, nothing is
|
|
9
|
+
* read from storage and nothing is written, so a self-hosted installation that
|
|
10
|
+
* never sets `enabled` makes no call to anybody.
|
|
11
|
+
*
|
|
12
|
+
* **A cached list paints at once.** The last answer is drawn while a fresh one is
|
|
13
|
+
* fetched, and kept if the fetch fails, marked `stale`. It is thirty days old at
|
|
14
|
+
* most and is checked as hard as a fresh answer, because storage is something
|
|
15
|
+
* anybody on the device can write.
|
|
16
|
+
*
|
|
17
|
+
* **The product's own route is the personal half.** The product's server holds the
|
|
18
|
+
* secret token and says who is signed in; this only ever calls that route, same
|
|
19
|
+
* origin, with the page's own cookie. The public key, if one is given, fetches
|
|
20
|
+
* the lower group straight from Identity so it can paint before the product has
|
|
21
|
+
* answered, and nothing more.
|
|
22
|
+
*/
|
|
23
|
+
import type { ChromeView } from "./types.js";
|
|
24
|
+
export type ChromeStatus = "off" | "idle" | "loading" | "ready" | "stale" | "error";
|
|
25
|
+
export interface ChromeState {
|
|
26
|
+
status: ChromeStatus;
|
|
27
|
+
view: ChromeView | null;
|
|
28
|
+
/** When `view` was last confirmed by Identity, in milliseconds since the epoch, or null. */
|
|
29
|
+
updatedAt: number | null;
|
|
30
|
+
/** Why the last refresh failed, short and for a quiet line rather than for a person. */
|
|
31
|
+
error: string | null;
|
|
32
|
+
}
|
|
33
|
+
/** The three calls a storage needs, which `localStorage` and `sessionStorage` both have. */
|
|
34
|
+
export interface StorageLike {
|
|
35
|
+
getItem(key: string): string | null;
|
|
36
|
+
setItem(key: string, value: string): void;
|
|
37
|
+
removeItem(key: string): void;
|
|
38
|
+
}
|
|
39
|
+
export declare const CACHE_KEY = "gpid.chrome.v1";
|
|
40
|
+
/** A list older than this is not drawn, however the fetch fares. */
|
|
41
|
+
export declare const CACHE_MAX_AGE_MS: number;
|
|
42
|
+
export declare const DEFAULT_TIMEOUT_MS = 5000;
|
|
43
|
+
export interface ChromeCoreOptions {
|
|
44
|
+
/** Nothing happens unless this is exactly `true`. */
|
|
45
|
+
enabled?: boolean;
|
|
46
|
+
/** The product's own route for the personal half, same origin. */
|
|
47
|
+
endpoint?: string;
|
|
48
|
+
/** Identity's address and this product's public key, for the lower group alone. */
|
|
49
|
+
baseUrl?: string;
|
|
50
|
+
publicKey?: string;
|
|
51
|
+
/** Where the last answer is kept. None means it is not kept. */
|
|
52
|
+
storage?: StorageLike | null;
|
|
53
|
+
cacheKey?: string;
|
|
54
|
+
fetch?: typeof fetch;
|
|
55
|
+
now?: () => number;
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
}
|
|
58
|
+
export interface ChromeCore {
|
|
59
|
+
state(): ChromeState;
|
|
60
|
+
subscribe(listener: (state: ChromeState) => void): () => void;
|
|
61
|
+
/** Fetch, update the cache and the state, and resolve with the state. Concurrent calls share one fetch. */
|
|
62
|
+
load(): Promise<ChromeState>;
|
|
63
|
+
/** Forget the cached list. A product calls it on sign-out, so a shared device never shows the next person the last one's products. */
|
|
64
|
+
clear(): void;
|
|
65
|
+
/** Stop: abort what is in flight and ignore what arrives. */
|
|
66
|
+
dispose(): void;
|
|
67
|
+
}
|
|
68
|
+
export declare function createChromeCore(options?: ChromeCoreOptions): ChromeCore;
|
|
69
|
+
//# sourceMappingURL=core.d.ts.map
|