@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/dist/tint.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The coloured initial's seven tints (sheet 32), and how one is picked.
3
+ *
4
+ * Tints of the seven product mark hues at L 0.84, C 0.07, with the initial in
5
+ * mark ink at 600: ink on tint measures 9.8 to 11.2, so the initial passes 4.5
6
+ * at any size on every host.
7
+ *
8
+ * **Picked by a stable id, never by the name**, so a person keeps their colour
9
+ * when they change their name and two people of the same name do not share one.
10
+ * Identity picks it from the Identity's id and sends only the result; a product
11
+ * hashes its own user id the same way for somebody with no Identity. Identity
12
+ * keeps a copy of `avatarTint` in step by hand, because this package is public
13
+ * and Identity does not depend on it: the vectors in `tint.test.ts` are the
14
+ * contract between the two.
15
+ */
16
+ /** Tide, Harbour, Iris, Rose, Ember, Amber, Moss: the order the index counts in. */
17
+ export declare const AVATAR_TINTS: readonly ["oklch(0.84 0.06 200)", "oklch(0.84 0.06 258)", "oklch(0.84 0.07 296)", "oklch(0.85 0.07 355)", "oklch(0.85 0.07 33)", "oklch(0.87 0.08 70)", "oklch(0.85 0.07 148)"];
18
+ /** Mark ink: the initial on every tint. */
19
+ export declare const AVATAR_INK = "#141c1e";
20
+ /** Which of the seven tints a stable id gets: FNV-1a over its UTF-16 code units, modulo seven. */
21
+ export declare function avatarTint(id: string): number;
22
+ //# sourceMappingURL=tint.d.ts.map
package/dist/tint.js ADDED
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * The coloured initial's seven tints (sheet 32), and how one is picked.
4
+ *
5
+ * Tints of the seven product mark hues at L 0.84, C 0.07, with the initial in
6
+ * mark ink at 600: ink on tint measures 9.8 to 11.2, so the initial passes 4.5
7
+ * at any size on every host.
8
+ *
9
+ * **Picked by a stable id, never by the name**, so a person keeps their colour
10
+ * when they change their name and two people of the same name do not share one.
11
+ * Identity picks it from the Identity's id and sends only the result; a product
12
+ * hashes its own user id the same way for somebody with no Identity. Identity
13
+ * keeps a copy of `avatarTint` in step by hand, because this package is public
14
+ * and Identity does not depend on it: the vectors in `tint.test.ts` are the
15
+ * contract between the two.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.AVATAR_INK = exports.AVATAR_TINTS = void 0;
19
+ exports.avatarTint = avatarTint;
20
+ /** Tide, Harbour, Iris, Rose, Ember, Amber, Moss: the order the index counts in. */
21
+ exports.AVATAR_TINTS = [
22
+ "oklch(0.84 0.06 200)",
23
+ "oklch(0.84 0.06 258)",
24
+ "oklch(0.84 0.07 296)",
25
+ "oklch(0.85 0.07 355)",
26
+ "oklch(0.85 0.07 33)",
27
+ "oklch(0.87 0.08 70)",
28
+ "oklch(0.85 0.07 148)",
29
+ ];
30
+ /** Mark ink: the initial on every tint. */
31
+ exports.AVATAR_INK = "#141c1e";
32
+ /** Which of the seven tints a stable id gets: FNV-1a over its UTF-16 code units, modulo seven. */
33
+ function avatarTint(id) {
34
+ let hash = 0x811c9dc5;
35
+ for (let i = 0; i < id.length; i += 1) {
36
+ hash ^= id.charCodeAt(i);
37
+ hash = Math.imul(hash, 0x01000193);
38
+ }
39
+ return (hash >>> 0) % exports.AVATAR_TINTS.length;
40
+ }
41
+ //# sourceMappingURL=tint.js.map
@@ -0,0 +1,83 @@
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
+ * Which of the seven avatar tints the Identity's id picks (0 to 6, `avatarTint`),
24
+ * so the coloured initial is the same in every product. Null without an Identity,
25
+ * and from an Identity too old to send it.
26
+ */
27
+ tint: number | null;
28
+ }
29
+ /**
30
+ * Where the account popup's own links go on Identity: the pill and the pencil.
31
+ * Identity sends them, like the footer, so its address can move without a
32
+ * release here. Null when they were not sent or any is not a web address, and
33
+ * then neither the pill nor the pencil is drawn.
34
+ */
35
+ export interface ChromeIdentityLinks {
36
+ /** "Manage your Identity account". */
37
+ accountUrl: string;
38
+ /** The pencil on the picture: the profile screen, where the picture is set. */
39
+ pictureUrl: string;
40
+ /** "Set up your Identity": registration, saying which product sent them and never carrying an address. */
41
+ setupUrl: string;
42
+ /** Identity's own mark for the pill, at an address that always leads to the current file. */
43
+ iconUrl: string | null;
44
+ }
45
+ /**
46
+ * The account popup's two footer links, the same in every product. Identity sends
47
+ * them, so a moved address needs no new release of this package. Null when they
48
+ * were not sent or either is not a web address, and then the footer is not drawn.
49
+ */
50
+ export interface ChromeLegal {
51
+ termsUrl: string;
52
+ privacyUrl: string;
53
+ }
54
+ /** The person, the products they have, the public ones they do not, Identity's own links and the footer. */
55
+ export interface ChromeView {
56
+ person: ChromePerson;
57
+ yours: ChromeTile[];
58
+ more: ChromeTile[];
59
+ identity: ChromeIdentityLinks | null;
60
+ legal: ChromeLegal | null;
61
+ }
62
+ /** The public half alone, for painting the lower group before the personal one arrives. */
63
+ export interface ChromePublicView {
64
+ more: ChromeTile[];
65
+ legal: ChromeLegal | null;
66
+ }
67
+ /**
68
+ * A product's name and marks as Identity keeps them: the one place they live, so
69
+ * a product reads its own here for its top bar, title and favicon, and a rename or
70
+ * a new mark reaches every product without a release.
71
+ */
72
+ export interface CatalogueEntry {
73
+ slug: string;
74
+ displayName: string;
75
+ homeUrl: string;
76
+ /** The colour mark, at an address that never changes its file. Null until one is uploaded. */
77
+ iconUrl: string | null;
78
+ /** The one colour mark, the same way. */
79
+ monoIconUrl: string | null;
80
+ /** When the entry or a mark last changed, ISO. */
81
+ updatedAt: string;
82
+ }
83
+ //# 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,148 @@
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
+ /** All three of Identity's links, or null, the same way as the footer. */
73
+ function identityOf(value) {
74
+ if (!isRecord(value))
75
+ return null;
76
+ const accountUrl = safeUrl(value["accountUrl"]);
77
+ const pictureUrl = safeUrl(value["pictureUrl"]);
78
+ const setupUrl = safeUrl(value["setupUrl"]);
79
+ if (accountUrl === null || pictureUrl === null || setupUrl === null)
80
+ return null;
81
+ const icon = value["iconUrl"];
82
+ return {
83
+ accountUrl,
84
+ pictureUrl,
85
+ setupUrl,
86
+ iconUrl: icon === null || icon === undefined ? null : safeUrl(icon),
87
+ };
88
+ }
89
+ /** One of the seven tints, or null for anything else. */
90
+ function tintOf(value) {
91
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value < 7
92
+ ? value
93
+ : null;
94
+ }
95
+ /** A view that can be drawn, or null where the answer is not one. */
96
+ function parseView(value) {
97
+ if (!isRecord(value) || !isRecord(value["person"]))
98
+ return null;
99
+ const person = value["person"];
100
+ const yours = tilesOf(value["yours"]);
101
+ const more = tilesOf(value["more"]);
102
+ if (yours === null || more === null)
103
+ return null;
104
+ if (typeof person["hasIdentity"] !== "boolean")
105
+ return null;
106
+ const avatar = person["avatarUrl"];
107
+ return {
108
+ person: {
109
+ hasIdentity: person["hasIdentity"],
110
+ avatarUrl: avatar === null || avatar === undefined ? null : safeUrl(avatar),
111
+ tint: tintOf(person["tint"]),
112
+ },
113
+ yours,
114
+ more,
115
+ identity: identityOf(value["identity"]),
116
+ legal: legalOf(value["legal"]),
117
+ };
118
+ }
119
+ function parsePublicView(value) {
120
+ if (!isRecord(value))
121
+ return null;
122
+ const more = tilesOf(value["more"]);
123
+ return more === null ? null : { more, legal: legalOf(value["legal"]) };
124
+ }
125
+ /** A catalogue entry that can be drawn, or null where the answer is not one. */
126
+ function parseCatalogueEntry(value) {
127
+ if (!isRecord(value))
128
+ return null;
129
+ const { slug, displayName, homeUrl, iconUrl, monoIconUrl, updatedAt } = value;
130
+ if (typeof slug !== "string" ||
131
+ slug === "" ||
132
+ typeof displayName !== "string" ||
133
+ displayName === "") {
134
+ return null;
135
+ }
136
+ const home = safeUrl(homeUrl);
137
+ if (home === null || typeof updatedAt !== "string")
138
+ return null;
139
+ return {
140
+ slug,
141
+ displayName,
142
+ homeUrl: home,
143
+ iconUrl: iconUrl === null || iconUrl === undefined ? null : safeUrl(iconUrl),
144
+ monoIconUrl: monoIconUrl === null || monoIconUrl === undefined ? null : safeUrl(monoIconUrl),
145
+ updatedAt,
146
+ };
147
+ }
148
+ //# sourceMappingURL=validate.js.map
package/package.json CHANGED
@@ -1,6 +1,64 @@
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.2.0",
4
+ "description": "The shared header controls of the Gelhaus Solutions products: the apps picker and the account popup, as custom elements with shadow DOM and as React wrappers. 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
+ "./react": {
25
+ "types": "./dist/react.d.ts",
26
+ "default": "./dist/react.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "!dist/.tsbuildinfo",
32
+ "!dist/**/*.map",
33
+ "LICENSE"
34
+ ],
35
+ "engines": {
36
+ "node": ">=22.0.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "peerDependencies": {
42
+ "react": ">=19"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "react": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.19.7",
51
+ "@types/react": "^19.3.0",
52
+ "jsdom": "^30.1.1",
53
+ "react": "19.2.8",
54
+ "typescript": "5.9.3",
55
+ "vitest": "^4.1.8"
56
+ },
57
+ "scripts": {
58
+ "build": "tsc -p tsconfig.build.json",
59
+ "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
60
+ "lint": "eslint \"src/**/*.ts\"",
61
+ "typecheck": "tsc --noEmit",
62
+ "test": "vitest run"
63
+ }
6
64
  }