@ghub/identity-chrome 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE 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,89 @@
1
- # Temporary Holding Version
1
+ # @ghub/identity-chrome
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The shared header controls of the Gelhaus Solutions products: the apps picker and the
4
+ account popup. This version is the part with no markup in it: the browser half (what the
5
+ controls know, what they keep, what they do when Identity cannot be reached) and the server
6
+ half (a product's own server asking GPlatform Identity). The Web Component and the React
7
+ wrapper come with the design.
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
+ ## A product's own name and mark
66
+
67
+ Identity's catalogue is where every product's name and mark live. Read your own from it for
68
+ your top bar, your title and your favicon, and a rename or a new mark reaches you without a
69
+ release:
70
+
71
+ ```ts
72
+ import { catalogueMarkUrl, createCatalogue } from "@ghub/identity-chrome";
73
+
74
+ const catalogue = createCatalogue({ baseUrl: process.env.GPID_URL });
75
+ const me = await catalogue.entry("gadvisory"); // { displayName, homeUrl, iconUrl, monoIconUrl, ... }
76
+
77
+ // Where you cannot ask first, an address that always leads to the current mark:
78
+ // <link rel="icon" type="image/svg+xml" href={catalogueMarkUrl(process.env.GPID_URL, "gadvisory")} />
79
+ ```
80
+
81
+ No token is needed. An entry is reused for a minute, and after a failed ask the last good one
82
+ is returned, so an outage leaves the name and mark you last had. A self-hosted install asks
83
+ nobody and ships the copy its release was built with.
84
+
85
+ The shapes are kept in step by hand with `ChromeView` and `CatalogueEntry` in GPlatform
86
+ Identity, which this public package cannot depend on.
87
+
88
+ Part of [GHub-Packages](https://github.com/Gelhaus-Solutions/GHub-Packages), the packages
89
+ 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
package/dist/core.js ADDED
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ /**
3
+ * The browser half: what the header controls know, and what they keep.
4
+ *
5
+ * It owns no markup. The apps picker and the account popup draw from `state()`
6
+ * and ask it to `load()`, which is what keeps one fetch, one cache and one
7
+ * failure story behind both controls.
8
+ *
9
+ * **Off unless enabled, and off means silent.** Nothing is fetched, nothing is
10
+ * read from storage and nothing is written, so a self-hosted installation that
11
+ * never sets `enabled` makes no call to anybody.
12
+ *
13
+ * **A cached list paints at once.** The last answer is drawn while a fresh one is
14
+ * fetched, and kept if the fetch fails, marked `stale`. It is thirty days old at
15
+ * most and is checked as hard as a fresh answer, because storage is something
16
+ * anybody on the device can write.
17
+ *
18
+ * **The product's own route is the personal half.** The product's server holds the
19
+ * secret token and says who is signed in; this only ever calls that route, same
20
+ * origin, with the page's own cookie. The public key, if one is given, fetches
21
+ * the lower group straight from Identity so it can paint before the product has
22
+ * answered, and nothing more.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.DEFAULT_TIMEOUT_MS = exports.CACHE_MAX_AGE_MS = exports.CACHE_KEY = void 0;
26
+ exports.createChromeCore = createChromeCore;
27
+ const validate_js_1 = require("./validate.js");
28
+ exports.CACHE_KEY = "gpid.chrome.v1";
29
+ /** A list older than this is not drawn, however the fetch fares. */
30
+ exports.CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
31
+ exports.DEFAULT_TIMEOUT_MS = 5000;
32
+ const NO_PERSON = { hasIdentity: false, avatarUrl: null };
33
+ function isRecord(value) {
34
+ return typeof value === "object" && value !== null && !Array.isArray(value);
35
+ }
36
+ function createChromeCore(options = {}) {
37
+ const enabled = options.enabled === true;
38
+ const now = options.now ?? Date.now;
39
+ const timeoutMs = options.timeoutMs ?? exports.DEFAULT_TIMEOUT_MS;
40
+ const storage = options.storage ?? null;
41
+ const cacheKey = options.cacheKey ?? exports.CACHE_KEY;
42
+ let state = {
43
+ status: enabled ? "idle" : "off",
44
+ view: null,
45
+ updatedAt: null,
46
+ error: null,
47
+ };
48
+ const listeners = new Set();
49
+ let inFlight = null;
50
+ let controller = null;
51
+ let disposed = false;
52
+ function set(next) {
53
+ if (disposed)
54
+ return;
55
+ state = { ...state, ...next };
56
+ for (const listener of [...listeners]) {
57
+ try {
58
+ listener(state);
59
+ }
60
+ catch {
61
+ // One listener's fault is not another's.
62
+ }
63
+ }
64
+ }
65
+ function readCache() {
66
+ if (storage === null)
67
+ return null;
68
+ try {
69
+ const raw = storage.getItem(cacheKey);
70
+ if (raw === null)
71
+ return null;
72
+ const parsed = JSON.parse(raw);
73
+ if (!isRecord(parsed) || typeof parsed["savedAt"] !== "number")
74
+ return null;
75
+ const savedAt = parsed["savedAt"];
76
+ // Too old, or dated in the future, which only somebody editing it would do.
77
+ if (now() - savedAt > exports.CACHE_MAX_AGE_MS || savedAt > now() + 60_000)
78
+ return null;
79
+ const view = (0, validate_js_1.parseView)(parsed["view"]);
80
+ return view === null ? null : { view, savedAt };
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ function writeCache(view, savedAt) {
87
+ if (storage === null)
88
+ return;
89
+ try {
90
+ storage.setItem(cacheKey, JSON.stringify({ v: 1, savedAt, view }));
91
+ }
92
+ catch {
93
+ // Full, blocked or private: the controls work without it.
94
+ }
95
+ }
96
+ async function getJson(url, init, signal) {
97
+ const send = options.fetch ?? globalThis.fetch.bind(globalThis);
98
+ const response = await send(url, { ...init, signal });
99
+ if (!response.ok)
100
+ throw new Error(`status ${String(response.status)}`);
101
+ try {
102
+ return await response.json();
103
+ }
104
+ catch {
105
+ throw new Error("invalid answer");
106
+ }
107
+ }
108
+ async function fetchPersonal(endpoint, signal) {
109
+ const body = await getJson(endpoint, { method: "GET", headers: { accept: "application/json" }, credentials: "same-origin" }, signal);
110
+ const view = (0, validate_js_1.parseView)(body);
111
+ if (view === null)
112
+ throw new Error("invalid answer");
113
+ return view;
114
+ }
115
+ async function fetchPublic(baseUrl, key, signal) {
116
+ const url = `${baseUrl.replace(/\/+$/u, "")}/api/v1/chrome/public`;
117
+ const body = await getJson(url, {
118
+ method: "GET",
119
+ headers: { accept: "application/json", "x-gpid-key": key },
120
+ credentials: "omit",
121
+ }, signal);
122
+ const view = (0, validate_js_1.parsePublicView)(body);
123
+ if (view === null)
124
+ throw new Error("invalid answer");
125
+ return view;
126
+ }
127
+ function reasonOf(error, timedOut) {
128
+ if (timedOut)
129
+ return "timeout";
130
+ if (error instanceof Error && /^(status \d+|invalid answer)$/u.test(error.message))
131
+ return error.message;
132
+ return "network";
133
+ }
134
+ async function run() {
135
+ const cached = readCache();
136
+ set({
137
+ status: "loading",
138
+ view: cached?.view ?? state.view,
139
+ updatedAt: cached?.savedAt ?? state.updatedAt,
140
+ error: null,
141
+ });
142
+ const own = new AbortController();
143
+ controller = own;
144
+ let timedOut = false;
145
+ const timer = setTimeout(() => {
146
+ timedOut = true;
147
+ own.abort();
148
+ }, timeoutMs);
149
+ try {
150
+ const personalCall = options.endpoint === undefined
151
+ ? Promise.resolve(null)
152
+ : fetchPersonal(options.endpoint, own.signal);
153
+ const publicCall = options.baseUrl === undefined || options.publicKey === undefined
154
+ ? Promise.resolve(null)
155
+ : fetchPublic(options.baseUrl, options.publicKey, own.signal);
156
+ // The lower group paints as soon as it is back, when nothing else is on screen yet.
157
+ void publicCall
158
+ .then((pub) => {
159
+ if (pub !== null && state.status === "loading" && state.view === null) {
160
+ set({ view: { person: NO_PERSON, yours: [], more: pub.more, legal: pub.legal } });
161
+ }
162
+ })
163
+ .catch(() => undefined);
164
+ const [personal, pub] = await Promise.allSettled([personalCall, publicCall]);
165
+ const publicHalf = pub.status === "fulfilled" ? pub.value : null;
166
+ if (personal.status === "fulfilled" && personal.value !== null) {
167
+ const at = now();
168
+ writeCache(personal.value, at);
169
+ set({ status: "ready", view: personal.value, updatedAt: at, error: null });
170
+ return state;
171
+ }
172
+ if (personal.status === "fulfilled" && publicHalf !== null) {
173
+ // No personal route configured: the public group is the whole answer.
174
+ set({
175
+ status: "ready",
176
+ view: { person: NO_PERSON, yours: [], ...publicHalf },
177
+ updatedAt: now(),
178
+ error: null,
179
+ });
180
+ return state;
181
+ }
182
+ const error = personal.status === "rejected" ? reasonOf(personal.reason, timedOut) : "not configured";
183
+ if (cached !== null) {
184
+ set({ status: "stale", view: cached.view, updatedAt: cached.savedAt, error });
185
+ }
186
+ else if (publicHalf !== null) {
187
+ set({
188
+ status: "error",
189
+ view: { person: NO_PERSON, yours: [], ...publicHalf },
190
+ updatedAt: null,
191
+ error,
192
+ });
193
+ }
194
+ else {
195
+ set({ status: "error", view: null, updatedAt: null, error });
196
+ }
197
+ return state;
198
+ }
199
+ finally {
200
+ clearTimeout(timer);
201
+ if (controller === own)
202
+ controller = null;
203
+ }
204
+ }
205
+ return {
206
+ state: () => state,
207
+ subscribe(listener) {
208
+ listeners.add(listener);
209
+ return () => {
210
+ listeners.delete(listener);
211
+ };
212
+ },
213
+ load() {
214
+ if (!enabled || disposed)
215
+ return Promise.resolve(state);
216
+ inFlight ??= run().finally(() => {
217
+ inFlight = null;
218
+ });
219
+ return inFlight;
220
+ },
221
+ clear() {
222
+ if (storage !== null) {
223
+ try {
224
+ storage.removeItem(cacheKey);
225
+ }
226
+ catch {
227
+ // Nothing to forget that could be read.
228
+ }
229
+ }
230
+ if (enabled)
231
+ set({ status: "idle", view: null, updatedAt: null, error: null });
232
+ },
233
+ dispose() {
234
+ disposed = true;
235
+ controller?.abort();
236
+ listeners.clear();
237
+ },
238
+ };
239
+ }
240
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1,12 @@
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. The server
6
+ * half, with the token, is `@ghub/identity-chrome/server`.
7
+ */
8
+ export { CACHE_KEY, CACHE_MAX_AGE_MS, DEFAULT_TIMEOUT_MS, createChromeCore, type ChromeCore, type ChromeCoreOptions, type ChromeState, type ChromeStatus, type StorageLike, } from "./core.js";
9
+ export { catalogueMarkUrl, createCatalogue, type Catalogue, type CatalogueOptions, } from "./catalogue.js";
10
+ export type { CatalogueEntry, ChromeLegal, ChromePerson, ChromePublicView, ChromeTile, ChromeView, } from "./types.js";
11
+ export { safeUrl } from "./validate.js";
12
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
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. The server
7
+ * half, with the token, is `@ghub/identity-chrome/server`.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.safeUrl = exports.createCatalogue = exports.catalogueMarkUrl = exports.createChromeCore = exports.DEFAULT_TIMEOUT_MS = exports.CACHE_MAX_AGE_MS = exports.CACHE_KEY = void 0;
11
+ var core_js_1 = require("./core.js");
12
+ Object.defineProperty(exports, "CACHE_KEY", { enumerable: true, get: function () { return core_js_1.CACHE_KEY; } });
13
+ Object.defineProperty(exports, "CACHE_MAX_AGE_MS", { enumerable: true, get: function () { return core_js_1.CACHE_MAX_AGE_MS; } });
14
+ Object.defineProperty(exports, "DEFAULT_TIMEOUT_MS", { enumerable: true, get: function () { return core_js_1.DEFAULT_TIMEOUT_MS; } });
15
+ Object.defineProperty(exports, "createChromeCore", { enumerable: true, get: function () { return core_js_1.createChromeCore; } });
16
+ var catalogue_js_1 = require("./catalogue.js");
17
+ Object.defineProperty(exports, "catalogueMarkUrl", { enumerable: true, get: function () { return catalogue_js_1.catalogueMarkUrl; } });
18
+ Object.defineProperty(exports, "createCatalogue", { enumerable: true, get: function () { return catalogue_js_1.createCatalogue; } });
19
+ var validate_js_1 = require("./validate.js");
20
+ Object.defineProperty(exports, "safeUrl", { enumerable: true, get: function () { return validate_js_1.safeUrl; } });
21
+ //# sourceMappingURL=index.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
@@ -0,0 +1,60 @@
1
+ /**
2
+ * What the header controls are told. These are the shapes GPlatform Identity
3
+ * answers with, kept in step by hand with `ChromeView` there: this package is
4
+ * public and cannot depend on that one, so a change to either is a change to
5
+ * both.
6
+ */
7
+ /** One tile in the apps picker. */
8
+ export interface ChromeTile {
9
+ slug: string;
10
+ displayName: string;
11
+ /** Where the tile goes. Only ever an http or https address: anything else is dropped on arrival. */
12
+ homeUrl: string;
13
+ /** Identity's address for the icon, or null until one is uploaded. */
14
+ iconUrl: string | null;
15
+ /** Whether this is the product the person is in. */
16
+ current: boolean;
17
+ }
18
+ /** What the person's own Identity adds to the account popup. Never a name: the product shows its own. */
19
+ export interface ChromePerson {
20
+ hasIdentity: boolean;
21
+ avatarUrl: string | null;
22
+ }
23
+ /**
24
+ * The account popup's two footer links, the same in every product. Identity sends
25
+ * them, so a moved address needs no new release of this package. Null when they
26
+ * were not sent or either is not a web address, and then the footer is not drawn.
27
+ */
28
+ export interface ChromeLegal {
29
+ termsUrl: string;
30
+ privacyUrl: string;
31
+ }
32
+ /** The person, the products they have, the public ones they do not, and the footer. */
33
+ export interface ChromeView {
34
+ person: ChromePerson;
35
+ yours: ChromeTile[];
36
+ more: ChromeTile[];
37
+ legal: ChromeLegal | null;
38
+ }
39
+ /** The public half alone, for painting the lower group before the personal one arrives. */
40
+ export interface ChromePublicView {
41
+ more: ChromeTile[];
42
+ legal: ChromeLegal | null;
43
+ }
44
+ /**
45
+ * A product's name and marks as Identity keeps them: the one place they live, so
46
+ * a product reads its own here for its top bar, title and favicon, and a rename or
47
+ * a new mark reaches every product without a release.
48
+ */
49
+ export interface CatalogueEntry {
50
+ slug: string;
51
+ displayName: string;
52
+ homeUrl: string;
53
+ /** The colour mark, at an address that never changes its file. Null until one is uploaded. */
54
+ iconUrl: string | null;
55
+ /** The one colour mark, the same way. */
56
+ monoIconUrl: string | null;
57
+ /** When the entry or a mark last changed, ISO. */
58
+ updatedAt: string;
59
+ }
60
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * What the header controls are told. These are the shapes GPlatform Identity
4
+ * answers with, kept in step by hand with `ChromeView` there: this package is
5
+ * public and cannot depend on that one, so a change to either is a change to
6
+ * both.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * What is believed about an answer before it is drawn.
3
+ *
4
+ * A tile's address becomes a link on every page of every product that mounts the
5
+ * controls, and an icon's address becomes an image source. Both come over the
6
+ * network, from a service that is trusted and not infallible, so the shape is
7
+ * checked and an address with any scheme but http or https is **dropped, not
8
+ * repaired**: a `javascript:` link is script on the host's page, and a tile that
9
+ * goes nowhere is a smaller fault than that.
10
+ *
11
+ * Applied to the cached copy as well as to a fresh answer, because the cache is
12
+ * storage anybody on the device can write.
13
+ */
14
+ import type { CatalogueEntry, ChromePublicView, ChromeView } from "./types.js";
15
+ /** An absolute http or https address, or null. */
16
+ export declare function safeUrl(value: unknown): string | null;
17
+ /** A view that can be drawn, or null where the answer is not one. */
18
+ export declare function parseView(value: unknown): ChromeView | null;
19
+ export declare function parsePublicView(value: unknown): ChromePublicView | null;
20
+ /** A catalogue entry that can be drawn, or null where the answer is not one. */
21
+ export declare function parseCatalogueEntry(value: unknown): CatalogueEntry | null;
22
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ /**
3
+ * What is believed about an answer before it is drawn.
4
+ *
5
+ * A tile's address becomes a link on every page of every product that mounts the
6
+ * controls, and an icon's address becomes an image source. Both come over the
7
+ * network, from a service that is trusted and not infallible, so the shape is
8
+ * checked and an address with any scheme but http or https is **dropped, not
9
+ * repaired**: a `javascript:` link is script on the host's page, and a tile that
10
+ * goes nowhere is a smaller fault than that.
11
+ *
12
+ * Applied to the cached copy as well as to a fresh answer, because the cache is
13
+ * storage anybody on the device can write.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.safeUrl = safeUrl;
17
+ exports.parseView = parseView;
18
+ exports.parsePublicView = parsePublicView;
19
+ exports.parseCatalogueEntry = parseCatalogueEntry;
20
+ function isRecord(value) {
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ }
23
+ /** An absolute http or https address, or null. */
24
+ function safeUrl(value) {
25
+ if (typeof value !== "string" || value.length > 2048)
26
+ return null;
27
+ try {
28
+ const url = new URL(value);
29
+ return url.protocol === "https:" || url.protocol === "http:" ? url.href : null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function tileOf(value) {
36
+ if (!isRecord(value))
37
+ return null;
38
+ const { slug, displayName, homeUrl, iconUrl, current } = value;
39
+ if (typeof slug !== "string" ||
40
+ slug === "" ||
41
+ typeof displayName !== "string" ||
42
+ displayName === "") {
43
+ return null;
44
+ }
45
+ const href = safeUrl(homeUrl);
46
+ if (href === null)
47
+ return null;
48
+ return {
49
+ slug,
50
+ displayName,
51
+ homeUrl: href,
52
+ iconUrl: iconUrl === null || iconUrl === undefined ? null : safeUrl(iconUrl),
53
+ current: current === true,
54
+ };
55
+ }
56
+ function tilesOf(value) {
57
+ if (!Array.isArray(value))
58
+ return null;
59
+ return value.map(tileOf).filter((tile) => tile !== null);
60
+ }
61
+ /**
62
+ * Both footer links, or null. Missing is not a broken answer: an older Identity or
63
+ * an older cached copy has none, and the popup draws no footer rather than nothing.
64
+ */
65
+ function legalOf(value) {
66
+ if (!isRecord(value))
67
+ return null;
68
+ const termsUrl = safeUrl(value["termsUrl"]);
69
+ const privacyUrl = safeUrl(value["privacyUrl"]);
70
+ return termsUrl === null || privacyUrl === null ? null : { termsUrl, privacyUrl };
71
+ }
72
+ /** A view that can be drawn, or null where the answer is not one. */
73
+ function parseView(value) {
74
+ if (!isRecord(value) || !isRecord(value["person"]))
75
+ return null;
76
+ const person = value["person"];
77
+ const yours = tilesOf(value["yours"]);
78
+ const more = tilesOf(value["more"]);
79
+ if (yours === null || more === null)
80
+ return null;
81
+ if (typeof person["hasIdentity"] !== "boolean")
82
+ return null;
83
+ const avatar = person["avatarUrl"];
84
+ return {
85
+ person: {
86
+ hasIdentity: person["hasIdentity"],
87
+ avatarUrl: avatar === null || avatar === undefined ? null : safeUrl(avatar),
88
+ },
89
+ yours,
90
+ more,
91
+ legal: legalOf(value["legal"]),
92
+ };
93
+ }
94
+ function parsePublicView(value) {
95
+ if (!isRecord(value))
96
+ return null;
97
+ const more = tilesOf(value["more"]);
98
+ return more === null ? null : { more, legal: legalOf(value["legal"]) };
99
+ }
100
+ /** A catalogue entry that can be drawn, or null where the answer is not one. */
101
+ function parseCatalogueEntry(value) {
102
+ if (!isRecord(value))
103
+ return null;
104
+ const { slug, displayName, homeUrl, iconUrl, monoIconUrl, updatedAt } = value;
105
+ if (typeof slug !== "string" ||
106
+ slug === "" ||
107
+ typeof displayName !== "string" ||
108
+ displayName === "") {
109
+ return null;
110
+ }
111
+ const home = safeUrl(homeUrl);
112
+ if (home === null || typeof updatedAt !== "string")
113
+ return null;
114
+ return {
115
+ slug,
116
+ displayName,
117
+ homeUrl: home,
118
+ iconUrl: iconUrl === null || iconUrl === undefined ? null : safeUrl(iconUrl),
119
+ monoIconUrl: monoIconUrl === null || monoIconUrl === undefined ? null : safeUrl(monoIconUrl),
120
+ updatedAt,
121
+ };
122
+ }
123
+ //# sourceMappingURL=validate.js.map
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@ghub/identity-chrome",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "The shared header controls of the Gelhaus Solutions products: the apps picker and the account popup. The browser half (state, cache, the last list held for an outage) and the server half (the token calls and a route handler). No dependencies.",
5
+ "author": "Gelhaus Solutions (Enno Gelhaus) <egelhaus@ennogelhaus.de> (https://ennogelhaus.de)",
6
+ "homepage": "https://ennogelhaus.de",
7
+ "license": "SEE LICENSE IN LICENSE",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Gelhaus-Solutions/GHub-Packages.git",
11
+ "directory": "packages/identity-chrome"
12
+ },
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./server": {
21
+ "types": "./dist/server.d.ts",
22
+ "default": "./dist/server.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "!dist/.tsbuildinfo",
28
+ "!dist/**/*.map",
29
+ "LICENSE"
30
+ ],
31
+ "engines": {
32
+ "node": ">=22.0.0"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.19.7",
39
+ "typescript": "5.9.3",
40
+ "vitest": "^4.1.8"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.build.json",
44
+ "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
45
+ "lint": "eslint \"src/**/*.ts\"",
46
+ "typecheck": "tsc --noEmit",
47
+ "test": "vitest run"
48
+ }
6
49
  }