@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.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The host contract: what the two elements read from the page they sit in.
3
+ *
4
+ * **The published `@ghub/tokens` names, and no family of their own.** The
5
+ * design drew a `--gc-*` set; the operator chose (2026-10-17) to read the
6
+ * names a host on `@ghub/tokens` already declares on its root, so such a host
7
+ * is styled with no setup at all. Custom properties inherit into a shadow
8
+ * root, which is the one thing that crosses it. A host on something else sets
9
+ * the same names.
10
+ *
11
+ * | Read | From | Notes |
12
+ * | --- | --- | --- |
13
+ * | the popup's surface | `--ga-menu`, else `--gm-overlay` | Aurora's frosted menu where the host is Aurora |
14
+ * | its frost | `--ga-frost-menu`, else none | |
15
+ * | its shadow | `--ga-shadow-menu`, else `--gm-shadow-overlay` | |
16
+ * | the solid surface | `--gm-overlay` | reduced transparency, the badge, the fade |
17
+ * | the account popup's tinted ground | `--ga-menu`, else `--gm-sunken` | the operator's pick (2026-10-17): exact to sheet 32 on a light host; on a dark one a tray a step below the page |
18
+ * | its solid ground | `--gm-sunken` | reduced transparency |
19
+ * | the account popup's panels | `--ga-track`, else `--gm-overlay` | Aurora's six per cent ink film, the white wash sheet 32 draws on glass |
20
+ * | everything else | `--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` | |
21
+ * | the face | `--font-sans` | |
22
+ *
23
+ * A host that declares none of them gets modern light, or modern dark under
24
+ * `prefers-color-scheme: dark`, the values sheet 31 draws its light and dark
25
+ * hosts with. No second accent: the popups take the host's.
26
+ */
27
+ /** The `:host` rules both elements start their sheet with. */
28
+ export declare const CONTRACT_CSS: string;
29
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * The host contract: what the two elements read from the page they sit in.
4
+ *
5
+ * **The published `@ghub/tokens` names, and no family of their own.** The
6
+ * design drew a `--gc-*` set; the operator chose (2026-10-17) to read the
7
+ * names a host on `@ghub/tokens` already declares on its root, so such a host
8
+ * is styled with no setup at all. Custom properties inherit into a shadow
9
+ * root, which is the one thing that crosses it. A host on something else sets
10
+ * the same names.
11
+ *
12
+ * | Read | From | Notes |
13
+ * | --- | --- | --- |
14
+ * | the popup's surface | `--ga-menu`, else `--gm-overlay` | Aurora's frosted menu where the host is Aurora |
15
+ * | its frost | `--ga-frost-menu`, else none | |
16
+ * | its shadow | `--ga-shadow-menu`, else `--gm-shadow-overlay` | |
17
+ * | the solid surface | `--gm-overlay` | reduced transparency, the badge, the fade |
18
+ * | the account popup's tinted ground | `--ga-menu`, else `--gm-sunken` | the operator's pick (2026-10-17): exact to sheet 32 on a light host; on a dark one a tray a step below the page |
19
+ * | its solid ground | `--gm-sunken` | reduced transparency |
20
+ * | the account popup's panels | `--ga-track`, else `--gm-overlay` | Aurora's six per cent ink film, the white wash sheet 32 draws on glass |
21
+ * | everything else | `--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` | |
22
+ * | the face | `--font-sans` | |
23
+ *
24
+ * A host that declares none of them gets modern light, or modern dark under
25
+ * `prefers-color-scheme: dark`, the values sheet 31 draws its light and dark
26
+ * hosts with. No second accent: the popups take the host's.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.CONTRACT_CSS = void 0;
30
+ const LIGHT = {
31
+ overlay: "#ffffff",
32
+ sunken: "oklch(0.955 0.004 258)",
33
+ plate: "oklch(0.97 0.003 258)",
34
+ ink: "oklch(0.19 0.008 258)",
35
+ ink2: "oklch(0.42 0.012 258)",
36
+ ink3: "oklch(0.53 0.014 258)",
37
+ hairline: "oklch(0 0 0 / 0.09)",
38
+ control: "oklch(0 0 0 / 0.48)",
39
+ hover: "oklch(0 0 0 / 0.045)",
40
+ active: "oklch(0 0 0 / 0.08)",
41
+ selected: "oklch(0.52 0.11 205 / 0.1)",
42
+ accent: "oklch(0.52 0.11 205)",
43
+ accentText: "oklch(0.5 0.11 205)",
44
+ ring: "oklch(0.52 0.11 205)",
45
+ critInk: "oklch(0.5 0.19 22)",
46
+ shadow: "0 0 0 1px oklch(0 0 0 / 0.07), 0 2px 4px oklch(0 0 0 / 0.04), 0 18px 40px -10px oklch(0 0 0 / 0.2)",
47
+ };
48
+ const DARK = {
49
+ overlay: "oklch(0.265 0.01 258)",
50
+ sunken: "oklch(0.155 0.007 258)",
51
+ plate: "oklch(0.235 0.009 258)",
52
+ ink: "oklch(0.975 0.003 255)",
53
+ ink2: "oklch(0.8 0.009 255)",
54
+ ink3: "oklch(0.645 0.012 258)",
55
+ hairline: "oklch(1 0 0 / 0.11)",
56
+ control: "oklch(1 0 0 / 0.36)",
57
+ hover: "oklch(1 0 0 / 0.06)",
58
+ active: "oklch(1 0 0 / 0.1)",
59
+ selected: "oklch(0.72 0.12 200 / 0.14)",
60
+ accent: "oklch(0.72 0.12 200)",
61
+ accentText: "oklch(0.78 0.11 200)",
62
+ ring: "oklch(0.82 0.11 196)",
63
+ critInk: "oklch(0.72 0.18 25)",
64
+ shadow: "inset 0 1px 0 oklch(1 0 0 / 0.08), 0 0 0 1px oklch(1 0 0 / 0.06), 0 24px 48px -12px oklch(0 0 0 / 0.6)",
65
+ };
66
+ /** The element's private names, each read from the host's published one with a fallback. */
67
+ function declarations(f) {
68
+ return `
69
+ --_overlay: var(--ga-menu, var(--gm-overlay, ${f.overlay}));
70
+ --_overlay-solid: var(--gm-overlay, ${f.overlay});
71
+ --_backdrop: var(--ga-frost-menu, none);
72
+ --_shadow: var(--ga-shadow-menu, var(--gm-shadow-overlay, ${f.shadow}));
73
+ --_tint: var(--ga-menu, var(--gm-sunken, ${f.sunken}));
74
+ --_tint-solid: var(--gm-sunken, ${f.sunken});
75
+ --_panel: var(--ga-track, var(--gm-overlay, ${f.overlay}));
76
+ --_plate: var(--gm-plate, ${f.plate});
77
+ --_ink: var(--gm-ink, ${f.ink});
78
+ --_ink-2: var(--gm-ink-2, ${f.ink2});
79
+ --_ink-3: var(--gm-ink-3, ${f.ink3});
80
+ --_hairline: var(--gm-hairline, ${f.hairline});
81
+ --_control: var(--gm-control, ${f.control});
82
+ --_hover: var(--gm-hover, ${f.hover});
83
+ --_active: var(--gm-active, ${f.active});
84
+ --_selected: var(--gm-selected, ${f.selected});
85
+ --_accent: var(--gm-accent, ${f.accent});
86
+ --_accent-text: var(--gm-accent-text, ${f.accentText});
87
+ --_ring: var(--gm-ring, ${f.ring});
88
+ --_crit-ink: var(--gm-crit-ink, ${f.critInk});
89
+ --_font: var(--font-sans, Geist, ui-sans-serif, system-ui, sans-serif);`;
90
+ }
91
+ /** The `:host` rules both elements start their sheet with. */
92
+ exports.CONTRACT_CSS = `
93
+ :host {${declarations(LIGHT)}
94
+ position: relative;
95
+ display: inline-block;
96
+ font-family: var(--_font);
97
+ -webkit-font-smoothing: antialiased;
98
+ }
99
+ @media (prefers-color-scheme: dark) {
100
+ :host {${declarations(DARK)}
101
+ }
102
+ }
103
+ :host([hidden]) { display: none; }
104
+ .sr-only {
105
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
106
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
107
+ }
108
+ `;
109
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The few DOM helpers both elements build their shadow trees with. Nothing here
3
+ * runs until an element is constructed, so importing it on a server is safe.
4
+ */
5
+ export declare const SVG_NS = "http://www.w3.org/2000/svg";
6
+ export declare function element<K extends keyof HTMLElementTagNameMap>(tag: K, attributes?: Record<string, string>, text?: string): HTMLElementTagNameMap[K];
7
+ /** An icon of one stroked or filled path, hidden from assistive technology. */
8
+ export declare function pathIcon(size: number, viewBox: string, d: string, stroke?: {
9
+ width: number;
10
+ } | null): SVGSVGElement;
11
+ /** The close cross both popups draw. */
12
+ export declare function crossMark(): SVGSVGElement;
13
+ //# sourceMappingURL=dom.d.ts.map
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ /**
3
+ * The few DOM helpers both elements build their shadow trees with. Nothing here
4
+ * runs until an element is constructed, so importing it on a server is safe.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.SVG_NS = void 0;
8
+ exports.element = element;
9
+ exports.pathIcon = pathIcon;
10
+ exports.crossMark = crossMark;
11
+ exports.SVG_NS = "http://www.w3.org/2000/svg";
12
+ function element(tag, attributes = {}, text) {
13
+ const made = document.createElement(tag);
14
+ for (const [name, value] of Object.entries(attributes))
15
+ made.setAttribute(name, value);
16
+ if (text !== undefined)
17
+ made.textContent = text;
18
+ return made;
19
+ }
20
+ /** An icon of one stroked or filled path, hidden from assistive technology. */
21
+ function pathIcon(size, viewBox, d, stroke = null) {
22
+ const svg = document.createElementNS(exports.SVG_NS, "svg");
23
+ svg.setAttribute("width", String(size));
24
+ svg.setAttribute("height", String(size));
25
+ svg.setAttribute("viewBox", viewBox);
26
+ svg.setAttribute("aria-hidden", "true");
27
+ const path = document.createElementNS(exports.SVG_NS, "path");
28
+ path.setAttribute("d", d);
29
+ if (stroke === null) {
30
+ path.setAttribute("fill", "currentColor");
31
+ }
32
+ else {
33
+ path.setAttribute("fill", "none");
34
+ path.setAttribute("stroke", "currentColor");
35
+ path.setAttribute("stroke-width", String(stroke.width));
36
+ path.setAttribute("stroke-linecap", "round");
37
+ }
38
+ svg.append(path);
39
+ return svg;
40
+ }
41
+ /** The close cross both popups draw. */
42
+ function crossMark() {
43
+ return pathIcon(14, "0 0 14 14", "M2 2l10 10M12 2L2 12", { width: 1.6 });
44
+ }
45
+ //# sourceMappingURL=dom.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The two header controls as custom elements, and the one call that defines both.
3
+ *
4
+ * Safe to import on a server: nothing touches the DOM until a define call runs
5
+ * in a browser.
6
+ */
7
+ import type { ChromeCore } from "../core.js";
8
+ import { type AccountLabels, type AccountOrganisation, type AccountPerson, type SignOutHandler, type SwitchHandler } from "./account.js";
9
+ import { type AppsLabels } from "./apps.js";
10
+ /** What `<gp-apps>` offers its page. */
11
+ export interface AppsElement extends HTMLElement {
12
+ get core(): ChromeCore | null;
13
+ set core(next: ChromeCore | null);
14
+ get labels(): AppsLabels;
15
+ set labels(next: Partial<AppsLabels>);
16
+ readonly open: boolean;
17
+ show(): void;
18
+ close(returnFocus: boolean): void;
19
+ }
20
+ /** What `<gp-account>` offers its page. */
21
+ export interface AccountElement extends HTMLElement {
22
+ get core(): ChromeCore | null;
23
+ set core(next: ChromeCore | null);
24
+ get person(): AccountPerson | null;
25
+ set person(next: AccountPerson | null);
26
+ get organisations(): AccountOrganisation[];
27
+ set organisations(next: AccountOrganisation[] | null);
28
+ product: string | null;
29
+ viaIdentity: boolean;
30
+ onSignOut: SignOutHandler | null;
31
+ onSwitch: SwitchHandler | null;
32
+ get labels(): AccountLabels;
33
+ set labels(next: Partial<AccountLabels>);
34
+ readonly open: boolean;
35
+ show(): void;
36
+ close(returnFocus: boolean): void;
37
+ }
38
+ declare global {
39
+ interface HTMLElementTagNameMap {
40
+ "gp-apps": AppsElement;
41
+ "gp-account": AccountElement;
42
+ }
43
+ }
44
+ /** Defines `<gp-apps>` and `<gp-account>`, each once. */
45
+ export declare function defineChromeElements(): void;
46
+ export { ACCOUNT_LABELS, createAccountElementClass, defineAccountElement, type AccountLabels, type AccountOrganisation, type AccountPerson, type OrganisationRole, type SignOutHandler, type SwitchHandler, } from "./account.js";
47
+ export { APPS_LABELS, createAppsElementClass, defineAppsElement, type AppsLabels } from "./apps.js";
48
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * The two header controls as custom elements, and the one call that defines both.
4
+ *
5
+ * Safe to import on a server: nothing touches the DOM until a define call runs
6
+ * in a browser.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.defineAppsElement = exports.createAppsElementClass = exports.APPS_LABELS = exports.defineAccountElement = exports.createAccountElementClass = exports.ACCOUNT_LABELS = void 0;
10
+ exports.defineChromeElements = defineChromeElements;
11
+ const account_js_1 = require("./account.js");
12
+ const apps_js_1 = require("./apps.js");
13
+ /** Defines `<gp-apps>` and `<gp-account>`, each once. */
14
+ function defineChromeElements() {
15
+ (0, apps_js_1.defineAppsElement)();
16
+ (0, account_js_1.defineAccountElement)();
17
+ }
18
+ var account_js_2 = require("./account.js");
19
+ Object.defineProperty(exports, "ACCOUNT_LABELS", { enumerable: true, get: function () { return account_js_2.ACCOUNT_LABELS; } });
20
+ Object.defineProperty(exports, "createAccountElementClass", { enumerable: true, get: function () { return account_js_2.createAccountElementClass; } });
21
+ Object.defineProperty(exports, "defineAccountElement", { enumerable: true, get: function () { return account_js_2.defineAccountElement; } });
22
+ var apps_js_2 = require("./apps.js");
23
+ Object.defineProperty(exports, "APPS_LABELS", { enumerable: true, get: function () { return apps_js_2.APPS_LABELS; } });
24
+ Object.defineProperty(exports, "createAppsElementClass", { enumerable: true, get: function () { return apps_js_2.createAppsElementClass; } });
25
+ Object.defineProperty(exports, "defineAppsElement", { enumerable: true, get: function () { return apps_js_2.defineAppsElement; } });
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @ghub/identity-chrome
3
+ *
4
+ * The shared header controls of the Gelhaus Solutions products. This entry is the
5
+ * browser half: state, cache and the last list held for an outage, and the two
6
+ * controls as custom elements (`<gp-apps>`, `<gp-account>`). The server half,
7
+ * with the token, is `@ghub/identity-chrome/server`; React wrappers are
8
+ * `@ghub/identity-chrome/react`.
9
+ */
10
+ export { CACHE_KEY, CACHE_MAX_AGE_MS, DEFAULT_TIMEOUT_MS, createChromeCore, type ChromeCore, type ChromeCoreOptions, type ChromeState, type ChromeStatus, type StorageLike, } from "./core.js";
11
+ export { catalogueMarkUrl, createCatalogue, type Catalogue, type CatalogueOptions, } from "./catalogue.js";
12
+ export { ACCOUNT_LABELS, APPS_LABELS, createAccountElementClass, createAppsElementClass, defineAccountElement, defineAppsElement, defineChromeElements, type AccountElement, type AccountLabels, type AccountOrganisation, type AccountPerson, type AppsElement, type AppsLabels, type OrganisationRole, type SignOutHandler, type SwitchHandler, } from "./elements/index.js";
13
+ export { AVATAR_INK, AVATAR_TINTS, avatarTint } from "./tint.js";
14
+ export type { CatalogueEntry, ChromeIdentityLinks, ChromeLegal, ChromePerson, ChromePublicView, ChromeTile, ChromeView, } from "./types.js";
15
+ export { safeUrl } from "./validate.js";
16
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /**
3
+ * @ghub/identity-chrome
4
+ *
5
+ * The shared header controls of the Gelhaus Solutions products. This entry is the
6
+ * browser half: state, cache and the last list held for an outage, and the two
7
+ * controls as custom elements (`<gp-apps>`, `<gp-account>`). The server half,
8
+ * with the token, is `@ghub/identity-chrome/server`; React wrappers are
9
+ * `@ghub/identity-chrome/react`.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.safeUrl = exports.avatarTint = exports.AVATAR_TINTS = exports.AVATAR_INK = exports.defineChromeElements = exports.defineAppsElement = exports.defineAccountElement = exports.createAppsElementClass = exports.createAccountElementClass = exports.APPS_LABELS = exports.ACCOUNT_LABELS = exports.createCatalogue = exports.catalogueMarkUrl = exports.createChromeCore = exports.DEFAULT_TIMEOUT_MS = exports.CACHE_MAX_AGE_MS = exports.CACHE_KEY = void 0;
13
+ var core_js_1 = require("./core.js");
14
+ Object.defineProperty(exports, "CACHE_KEY", { enumerable: true, get: function () { return core_js_1.CACHE_KEY; } });
15
+ Object.defineProperty(exports, "CACHE_MAX_AGE_MS", { enumerable: true, get: function () { return core_js_1.CACHE_MAX_AGE_MS; } });
16
+ Object.defineProperty(exports, "DEFAULT_TIMEOUT_MS", { enumerable: true, get: function () { return core_js_1.DEFAULT_TIMEOUT_MS; } });
17
+ Object.defineProperty(exports, "createChromeCore", { enumerable: true, get: function () { return core_js_1.createChromeCore; } });
18
+ var catalogue_js_1 = require("./catalogue.js");
19
+ Object.defineProperty(exports, "catalogueMarkUrl", { enumerable: true, get: function () { return catalogue_js_1.catalogueMarkUrl; } });
20
+ Object.defineProperty(exports, "createCatalogue", { enumerable: true, get: function () { return catalogue_js_1.createCatalogue; } });
21
+ var index_js_1 = require("./elements/index.js");
22
+ Object.defineProperty(exports, "ACCOUNT_LABELS", { enumerable: true, get: function () { return index_js_1.ACCOUNT_LABELS; } });
23
+ Object.defineProperty(exports, "APPS_LABELS", { enumerable: true, get: function () { return index_js_1.APPS_LABELS; } });
24
+ Object.defineProperty(exports, "createAccountElementClass", { enumerable: true, get: function () { return index_js_1.createAccountElementClass; } });
25
+ Object.defineProperty(exports, "createAppsElementClass", { enumerable: true, get: function () { return index_js_1.createAppsElementClass; } });
26
+ Object.defineProperty(exports, "defineAccountElement", { enumerable: true, get: function () { return index_js_1.defineAccountElement; } });
27
+ Object.defineProperty(exports, "defineAppsElement", { enumerable: true, get: function () { return index_js_1.defineAppsElement; } });
28
+ Object.defineProperty(exports, "defineChromeElements", { enumerable: true, get: function () { return index_js_1.defineChromeElements; } });
29
+ var tint_js_1 = require("./tint.js");
30
+ Object.defineProperty(exports, "AVATAR_INK", { enumerable: true, get: function () { return tint_js_1.AVATAR_INK; } });
31
+ Object.defineProperty(exports, "AVATAR_TINTS", { enumerable: true, get: function () { return tint_js_1.AVATAR_TINTS; } });
32
+ Object.defineProperty(exports, "avatarTint", { enumerable: true, get: function () { return tint_js_1.avatarTint; } });
33
+ var validate_js_1 = require("./validate.js");
34
+ Object.defineProperty(exports, "safeUrl", { enumerable: true, get: function () { return validate_js_1.safeUrl; } });
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @ghub/identity-chrome/react
3
+ *
4
+ * The two header controls for a React page: each renders its custom element and
5
+ * hands it what it needs as properties after mounting, because a core, a person
6
+ * and a callback are objects and an attribute can carry only a string. The
7
+ * elements do the drawing, the keyboard and the accessibility; these wrappers
8
+ * only connect them.
9
+ */
10
+ import { type ReactElement } from "react";
11
+ import type { ChromeCore } from "./core.js";
12
+ import type { AccountLabels, AccountOrganisation, AccountPerson, AppsLabels, SignOutHandler, SwitchHandler } from "./elements/index.js";
13
+ /** Which edge of the trigger the popup lines up with. The end, by default. */
14
+ type Align = "start" | "end";
15
+ export interface GpAppsProps {
16
+ core: ChromeCore | null;
17
+ labels?: Partial<AppsLabels>;
18
+ align?: Align;
19
+ className?: string;
20
+ }
21
+ /** `<gp-apps>`, the apps picker. */
22
+ export declare function GpApps({ core, labels, align, className }: GpAppsProps): ReactElement;
23
+ export interface GpAccountProps {
24
+ core: ChromeCore | null;
25
+ /** Who is signed in, from the product's own session. Nothing renders without one. */
26
+ person: AccountPerson | null;
27
+ organisations?: AccountOrganisation[] | null;
28
+ /** The product's own name, from its catalogue entry. */
29
+ product?: string | null;
30
+ /** The product signs in through Identity, so signing out ends both. */
31
+ viaIdentity?: boolean;
32
+ onSignOut?: SignOutHandler | null;
33
+ onSwitch?: SwitchHandler | null;
34
+ labels?: Partial<AccountLabels>;
35
+ align?: Align;
36
+ className?: string;
37
+ }
38
+ /** `<gp-account>`, the account popup. */
39
+ export declare function GpAccount({ core, person, organisations, product, viaIdentity, onSignOut, onSwitch, labels, align, className, }: GpAccountProps): ReactElement;
40
+ export {};
41
+ //# sourceMappingURL=react.d.ts.map
package/dist/react.js ADDED
@@ -0,0 +1,73 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.GpApps = GpApps;
5
+ exports.GpAccount = GpAccount;
6
+ /**
7
+ * @ghub/identity-chrome/react
8
+ *
9
+ * The two header controls for a React page: each renders its custom element and
10
+ * hands it what it needs as properties after mounting, because a core, a person
11
+ * and a callback are objects and an attribute can carry only a string. The
12
+ * elements do the drawing, the keyboard and the accessibility; these wrappers
13
+ * only connect them.
14
+ */
15
+ const react_1 = require("react");
16
+ const index_js_1 = require("./elements/index.js");
17
+ /** `<gp-apps>`, the apps picker. */
18
+ function GpApps({ core, labels, align, className }) {
19
+ const ref = (0, react_1.useRef)(null);
20
+ (0, react_1.useEffect)(() => {
21
+ (0, index_js_1.defineChromeElements)();
22
+ }, []);
23
+ (0, react_1.useEffect)(() => {
24
+ if (ref.current !== null)
25
+ ref.current.core = core;
26
+ }, [core]);
27
+ (0, react_1.useEffect)(() => {
28
+ if (ref.current !== null && labels !== undefined)
29
+ ref.current.labels = labels;
30
+ }, [labels]);
31
+ return (0, react_1.createElement)("gp-apps", { ref, className, align });
32
+ }
33
+ /** `<gp-account>`, the account popup. */
34
+ function GpAccount({ core, person, organisations = null, product = null, viaIdentity = false, onSignOut = null, onSwitch = null, labels, align, className, }) {
35
+ const ref = (0, react_1.useRef)(null);
36
+ (0, react_1.useEffect)(() => {
37
+ (0, index_js_1.defineChromeElements)();
38
+ }, []);
39
+ (0, react_1.useEffect)(() => {
40
+ if (ref.current !== null)
41
+ ref.current.core = core;
42
+ }, [core]);
43
+ (0, react_1.useEffect)(() => {
44
+ if (ref.current !== null)
45
+ ref.current.person = person;
46
+ }, [person]);
47
+ (0, react_1.useEffect)(() => {
48
+ if (ref.current !== null)
49
+ ref.current.organisations = organisations;
50
+ }, [organisations]);
51
+ (0, react_1.useEffect)(() => {
52
+ if (ref.current !== null)
53
+ ref.current.product = product;
54
+ }, [product]);
55
+ (0, react_1.useEffect)(() => {
56
+ if (ref.current !== null)
57
+ ref.current.viaIdentity = viaIdentity;
58
+ }, [viaIdentity]);
59
+ (0, react_1.useEffect)(() => {
60
+ if (ref.current !== null)
61
+ ref.current.onSignOut = onSignOut;
62
+ }, [onSignOut]);
63
+ (0, react_1.useEffect)(() => {
64
+ if (ref.current !== null)
65
+ ref.current.onSwitch = onSwitch;
66
+ }, [onSwitch]);
67
+ (0, react_1.useEffect)(() => {
68
+ if (ref.current !== null && labels !== undefined)
69
+ ref.current.labels = labels;
70
+ }, [labels]);
71
+ return (0, react_1.createElement)("gp-account", { ref, className, align });
72
+ }
73
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The server half: a product's own server speaking to Identity for the header
3
+ * controls.
4
+ *
5
+ * The secret token lives here and nowhere near a browser. The page asks the
6
+ * product's own route; that route knows who is signed in, from the product's own
7
+ * session, and asks Identity about that one person by verified email. Identity
8
+ * answers with the products they have and the public ones they do not.
9
+ *
10
+ * **Say only who is signed in, and only what the product has verified.** The
11
+ * address goes to Identity as the thing the person's products are joined on, so
12
+ * an address the product has not verified is a way to ask about somebody else.
13
+ *
14
+ * **Never log the token, and never put it in an error.** Everything thrown here
15
+ * says what happened and nothing about what was sent.
16
+ */
17
+ import type { ChromeView } from "./types.js";
18
+ /** A refusal or a failure from Identity, with how long to wait when it said. */
19
+ export declare class ChromeError extends Error {
20
+ readonly status: number;
21
+ readonly retryAfterSeconds?: number | undefined;
22
+ constructor(status: number, message: string, retryAfterSeconds?: number | undefined);
23
+ }
24
+ export interface ChromeServerOptions {
25
+ /** Identity's public address, with or without a trailing slash. */
26
+ baseUrl: string;
27
+ /** This product's slug in the catalogue. */
28
+ product: string;
29
+ /** The secret token staff issued for this product. Never sent to a browser. */
30
+ token: string;
31
+ fetch?: typeof fetch;
32
+ /** Milliseconds. A call that hangs must not hang a page render. */
33
+ timeoutMs?: number;
34
+ /** How long one person's answer is reused, in seconds. Zero asks every time. */
35
+ viewCacheSeconds?: number;
36
+ now?: () => number;
37
+ }
38
+ export interface ChromeServer {
39
+ /** The products of the person signed in, as Identity knows them. Counts as a visit to this product. */
40
+ view(email: string): Promise<ChromeView>;
41
+ /** The one-off push of this product's users, inside the window staff opened. Batched for you. */
42
+ seed(emails: readonly string[]): Promise<{
43
+ changed: number;
44
+ }>;
45
+ /** Say these people no longer have access to this product. */
46
+ remove(emails: readonly string[]): Promise<{
47
+ changed: number;
48
+ }>;
49
+ /** Drop what is held for one person, on sign-out. */
50
+ forget(email: string): void;
51
+ /**
52
+ * A route for any framework that speaks the web `Request` and `Response`
53
+ * (Next route handlers, Hono, Bun, Deno, Cloudflare). `who` says who is
54
+ * signed in, from the product's own session, or null for nobody.
55
+ */
56
+ handler(who: (request: Request) => Promise<{
57
+ email: string;
58
+ } | null> | {
59
+ email: string;
60
+ } | null): (request: Request) => Promise<Response>;
61
+ }
62
+ /** One request to Identity carries at most this many addresses. */
63
+ export declare const BATCH = 1000;
64
+ export declare function createChromeServer(options: ChromeServerOptions): ChromeServer;
65
+ //# sourceMappingURL=server.d.ts.map
package/dist/server.js ADDED
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ /**
3
+ * The server half: a product's own server speaking to Identity for the header
4
+ * controls.
5
+ *
6
+ * The secret token lives here and nowhere near a browser. The page asks the
7
+ * product's own route; that route knows who is signed in, from the product's own
8
+ * session, and asks Identity about that one person by verified email. Identity
9
+ * answers with the products they have and the public ones they do not.
10
+ *
11
+ * **Say only who is signed in, and only what the product has verified.** The
12
+ * address goes to Identity as the thing the person's products are joined on, so
13
+ * an address the product has not verified is a way to ask about somebody else.
14
+ *
15
+ * **Never log the token, and never put it in an error.** Everything thrown here
16
+ * says what happened and nothing about what was sent.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.BATCH = exports.ChromeError = void 0;
20
+ exports.createChromeServer = createChromeServer;
21
+ const validate_js_1 = require("./validate.js");
22
+ /** A refusal or a failure from Identity, with how long to wait when it said. */
23
+ class ChromeError extends Error {
24
+ status;
25
+ retryAfterSeconds;
26
+ constructor(status, message, retryAfterSeconds) {
27
+ super(message);
28
+ this.status = status;
29
+ this.retryAfterSeconds = retryAfterSeconds;
30
+ this.name = "ChromeError";
31
+ }
32
+ }
33
+ exports.ChromeError = ChromeError;
34
+ /** One request to Identity carries at most this many addresses. */
35
+ exports.BATCH = 1000;
36
+ const MAX_HELD = 500;
37
+ const DEFAULT_TIMEOUT_MS = 4000;
38
+ const DEFAULT_CACHE_SECONDS = 60;
39
+ function createChromeServer(options) {
40
+ const base = options.baseUrl.replace(/\/+$/u, "");
41
+ const now = options.now ?? Date.now;
42
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
43
+ const ttlMs = (options.viewCacheSeconds ?? DEFAULT_CACHE_SECONDS) * 1000;
44
+ const held = new Map();
45
+ const key = (email) => email.trim().normalize("NFC").toLowerCase();
46
+ async function call(method, path, body) {
47
+ const send = options.fetch ?? globalThis.fetch.bind(globalThis);
48
+ const controller = new AbortController();
49
+ const timer = setTimeout(() => {
50
+ controller.abort();
51
+ }, timeoutMs);
52
+ let response;
53
+ try {
54
+ response = await send(`${base}${path}`, {
55
+ method,
56
+ headers: {
57
+ accept: "application/json",
58
+ "content-type": "application/json",
59
+ "x-gpid-product": options.product,
60
+ authorization: `Bearer ${options.token}`,
61
+ },
62
+ body: JSON.stringify(body),
63
+ signal: controller.signal,
64
+ });
65
+ }
66
+ catch {
67
+ throw new ChromeError(0, "Identity could not be reached");
68
+ }
69
+ finally {
70
+ clearTimeout(timer);
71
+ }
72
+ if (!response.ok) {
73
+ const retry = Number(response.headers.get("retry-after"));
74
+ throw new ChromeError(response.status, `Identity answered ${String(response.status)}`, Number.isFinite(retry) && retry > 0 ? retry : undefined);
75
+ }
76
+ try {
77
+ return await response.json();
78
+ }
79
+ catch {
80
+ throw new ChromeError(502, "Identity's answer was not understood");
81
+ }
82
+ }
83
+ async function batched(method, emails) {
84
+ let changed = 0;
85
+ for (let start = 0; start < emails.length; start += exports.BATCH) {
86
+ const answer = await call(method, "/api/v1/chrome/access", {
87
+ emails: emails.slice(start, start + exports.BATCH),
88
+ });
89
+ const count = answer.changed;
90
+ changed += typeof count === "number" ? count : 0;
91
+ }
92
+ return { changed };
93
+ }
94
+ const server = {
95
+ async view(email) {
96
+ const id = key(email);
97
+ const reuse = held.get(id);
98
+ if (reuse !== undefined && now() - reuse.at < ttlMs)
99
+ return reuse.view;
100
+ const view = (0, validate_js_1.parseView)(await call("POST", "/api/v1/chrome/view", { email }));
101
+ if (view === null)
102
+ throw new ChromeError(502, "Identity's answer was not understood");
103
+ // Newest last, so the oldest is the one to go when the table is full.
104
+ held.delete(id);
105
+ held.set(id, { view, at: now() });
106
+ if (held.size > MAX_HELD) {
107
+ const oldest = held.keys().next().value;
108
+ if (oldest !== undefined)
109
+ held.delete(oldest);
110
+ }
111
+ return view;
112
+ },
113
+ seed: (emails) => batched("PUT", emails),
114
+ remove: (emails) => batched("DELETE", emails),
115
+ forget(email) {
116
+ held.delete(key(email));
117
+ },
118
+ handler(who) {
119
+ return async (request) => {
120
+ const json = (status, body, headers = {}) => new Response(JSON.stringify(body), {
121
+ status,
122
+ headers: {
123
+ "content-type": "application/json",
124
+ "cache-control": "private, no-store",
125
+ ...headers,
126
+ },
127
+ });
128
+ if (request.method !== "GET")
129
+ return json(405, { error: "method not allowed" }, { allow: "GET" });
130
+ const person = await who(request);
131
+ if (person === null)
132
+ return json(401, { error: "not signed in" });
133
+ try {
134
+ return json(200, await server.view(person.email));
135
+ }
136
+ catch (error) {
137
+ // Whatever went wrong, the page keeps the list it held. Nothing about why leaves here.
138
+ const wait = error instanceof ChromeError && error.status === 429
139
+ ? (error.retryAfterSeconds ?? 60)
140
+ : 30;
141
+ return json(503, { error: "unavailable" }, { "retry-after": String(wait) });
142
+ }
143
+ };
144
+ },
145
+ };
146
+ return server;
147
+ }
148
+ //# sourceMappingURL=server.js.map