@herazgo/sdk 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/README.md CHANGED
@@ -1,3 +1,75 @@
1
- # Temporary Holding Version
1
+ # @herazgo/sdk
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
+ Typings and install helpers for the Herazgo embeddable script. The widget
4
+ runtime always loads from the Herazgo CDN (`static.herazgo.com/hg.js`) — this
5
+ package ships no widget code, so the snippet stays the canonical install.
6
+
7
+ ## What's in it
8
+
9
+ - `HGGlobal` / `HGModuleApi` — the typed `window.HG` contract; a global
10
+ `Window.HG` declaration ships with the types.
11
+ - `loadHG({ key })` — injects the same snippet and resolves when the loader
12
+ marker (`HG.__herazgo`) or a requested module sub-API exists.
13
+ - `seedHG({ feedback: { hidden: true } })` — typed pre-seed for first-render
14
+ suppression; call before the snippet / `loadHG()`.
15
+
16
+ ## Usage
17
+
18
+ Snippet-and-types only (no code import):
19
+
20
+ ```sh
21
+ pnpm add @herazgo/sdk
22
+ ```
23
+
24
+ ```jsonc
25
+ // tsconfig.json
26
+ "compilerOptions": { "types": ["@herazgo/sdk"] }
27
+ ```
28
+
29
+ Managed loading:
30
+
31
+ ```ts
32
+ import { loadHG } from "@herazgo/sdk";
33
+ await loadHG({ key: "hz-XXXXXXXXXX", module: "feedback" });
34
+ HG.feedback?.open(); // typed
35
+ ```
36
+
37
+ Suppressed launcher (before loading):
38
+
39
+ ```ts
40
+ import { seedHG, loadHG } from "@herazgo/sdk";
41
+ seedHG({ feedback: { hidden: true } });
42
+ await loadHG({ key: "hz-XXXXXXXXXX", module: "feedback" });
43
+ HG.feedback?.show();
44
+ ```
45
+
46
+ ## Always-safe facade (default export)
47
+
48
+ ```ts
49
+ import HG from "@herazgo/sdk";
50
+ HG.feedback.open(); // safe anytime
51
+ ```
52
+
53
+ The facade is a view of the live `window.HG` API, resolved per call. Before
54
+ the snippet runs it never throws: `open()` is deferred (drains when the
55
+ widget lands), `on()` subscriptions are buffered (the returned unsubscribe
56
+ cancels in either phase), `hide()`/`show()` are vacuous, `opened`/`hidden`
57
+ read `false`. Each pre-load call path warns once (subsequent calls stay
58
+ quiet); `HG.module("name")` builds the same safe stub for any other module.
59
+ Server-side (no window) everything is silently inert, so SSR code can call
60
+ freely — nothing is logged and intents are dropped.
61
+
62
+ The facade never becomes `window.HG` and never touches the loader's
63
+ claim/lock semantics; snippet apps can keep using typed-optional
64
+ `window.HG`. Multi-instance hosts that need isolated state use `createHG()`.
65
+
66
+ ## Notes
67
+
68
+ - `loadHG` is idempotent; the loader itself dedupes double loads
69
+ (`window.__herazgoLoaded`). If a plain snippet is already present, tagging
70
+ it `data-hg-key` is optional — a race injects a second script that no-ops.
71
+ - `module: "feedback"` resolves when `HG.feedback` exists — enabled modules
72
+ only; a wrong or disabled module name times out (default 10s).
73
+ - The published contract is compiled against the loader's own sub-API
74
+ (`satisfies HGModuleApi` in the sdk repo's `src/host-api.ts`), so loader
75
+ drift fails the sdk repo's typecheck before release.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The npm-path facade (`HG.feedback.open()` is safe pre-load by contract):
3
+ * every member resolves the live `window.HG` API per call — delegates when
4
+ * the snippet has run, no-ops + one-time warn when it hasn't. Never throws,
5
+ * never touches the loader's claim/lock semantics (this is not `window.HG`;
6
+ * it's a view of it). `open()` and `on()` intents buffer and drain once the
7
+ * live API appears; `hide()`/`show()` pre-live are vacuous (nothing mounted).
8
+ * Server-side (no window) everything is silently inert — SSR calls log
9
+ * nothing.
10
+ */
11
+ import type { HGModuleApi } from "./host-api-types.js";
12
+ export interface HGFacade {
13
+ /** Feedback module sub-API — safe stub until the live API exists. */
14
+ readonly feedback: HGModuleApi;
15
+ /** Safe sub-API for any other module name (cached per name). */
16
+ module(name: string): HGModuleApi;
17
+ }
18
+ /**
19
+ * Builds a facade. The default export is one long-lived instance; npm tests
20
+ * and multi-instance hosts use the factory to get isolated state.
21
+ */
22
+ export declare function createHG(): HGFacade;
23
+ /** The shared facade instance behind the package's default export. */
24
+ declare const HG: HGFacade;
25
+ export default HG;
package/lib/facade.js ADDED
@@ -0,0 +1,137 @@
1
+ /** Live module API off window.HG, or undefined while absent. */
2
+ function liveModule(name) {
3
+ if (typeof window === "undefined")
4
+ return undefined;
5
+ const live = window.HG?.[name];
6
+ return liveObject(live) ? live : undefined;
7
+ }
8
+ function liveObject(value) {
9
+ return typeof value === "object" && value != null;
10
+ }
11
+ function warnOnce(slot, name, method) {
12
+ if (typeof window === "undefined")
13
+ return; // SSR: silent everywhere
14
+ if (slot.warned.has(method))
15
+ return;
16
+ slot.warned.add(method);
17
+ console.warn(`[herazgo] ${name}.${method}() before the Herazgo snippet loaded — ` +
18
+ (method === "open" || method === "on" ? "deferred until it loads" : "ignored"));
19
+ }
20
+ /**
21
+ * Builds a facade. The default export is one long-lived instance; npm tests
22
+ * and multi-instance hosts use the factory to get isolated state.
23
+ */
24
+ export function createHG() {
25
+ const slots = new Map();
26
+ let poller;
27
+ const slotFor = (name) => {
28
+ const existing = slots.get(name);
29
+ if (existing)
30
+ return existing;
31
+ const slot = { openRequested: false, buffered: [], warned: new Set() };
32
+ slots.set(name, slot);
33
+ return slot;
34
+ };
35
+ /** Drains buffered intents once the live API exists; stops itself when no
36
+ * slot has pending state left. */
37
+ const ensurePoller = () => {
38
+ poller ?? (poller = setInterval(() => {
39
+ let pending = false;
40
+ for (const [name, slot] of slots) {
41
+ const live = liveModule(name);
42
+ if (live == null) {
43
+ pending = pending || slot.openRequested || slot.buffered.length > 0;
44
+ continue;
45
+ }
46
+ if (slot.openRequested) {
47
+ slot.openRequested = false;
48
+ live.open();
49
+ }
50
+ for (const entry of slot.buffered.splice(0)) {
51
+ entry.unsub = live.on(entry.type, entry.cb);
52
+ }
53
+ }
54
+ if (!pending) {
55
+ clearInterval(poller);
56
+ poller = undefined;
57
+ }
58
+ }, 200));
59
+ };
60
+ /** The safe sub-API for one module name: per-call delegation. */
61
+ const apiFor = (name) => ({
62
+ get hidden() {
63
+ return liveModule(name)?.hidden ?? false;
64
+ },
65
+ get opened() {
66
+ return liveModule(name)?.opened ?? false;
67
+ },
68
+ hide() {
69
+ const live = liveModule(name);
70
+ if (live != null)
71
+ return live.hide();
72
+ warnOnce(slotFor(name), name, "hide");
73
+ },
74
+ show() {
75
+ const live = liveModule(name);
76
+ if (live != null)
77
+ return live.show();
78
+ // pre-live there is nothing suppressed that hide() would have set
79
+ },
80
+ open() {
81
+ if (typeof window === "undefined")
82
+ return; // SSR: intents are lost
83
+ const live = liveModule(name);
84
+ const slot = slotFor(name);
85
+ if (live != null)
86
+ return live.open();
87
+ warnOnce(slot, name, "open");
88
+ slot.openRequested = true;
89
+ ensurePoller();
90
+ },
91
+ close() {
92
+ // last call wins: a close pre-live also cancels a buffered open
93
+ slotFor(name).openRequested = false;
94
+ liveModule(name)?.close();
95
+ },
96
+ on(type, cb) {
97
+ if (typeof window === "undefined")
98
+ return () => { }; // SSR: silent no-op
99
+ const slot = slotFor(name);
100
+ const live = liveModule(name);
101
+ if (live != null)
102
+ return live.on(type, cb);
103
+ const entry = { type, cb };
104
+ slot.buffered.push(entry);
105
+ warnOnce(slot, name, "on");
106
+ ensurePoller();
107
+ return () => {
108
+ // cancel pre-drain or live subscription, whichever phase we're in
109
+ if (entry.unsub != null) {
110
+ entry.unsub();
111
+ entry.unsub = undefined;
112
+ }
113
+ const at = slot.buffered.indexOf(entry);
114
+ if (at >= 0)
115
+ slot.buffered.splice(at, 1);
116
+ };
117
+ },
118
+ });
119
+ const feedbackCache = apiFor("feedback");
120
+ const otherCache = new Map();
121
+ return {
122
+ get feedback() {
123
+ return feedbackCache;
124
+ },
125
+ module(name) {
126
+ const cached = otherCache.get(name);
127
+ if (cached)
128
+ return cached;
129
+ const api = apiFor(name);
130
+ otherCache.set(name, api);
131
+ return api;
132
+ },
133
+ };
134
+ }
135
+ /** The shared facade instance behind the package's default export. */
136
+ const HG = createHG();
137
+ export default HG;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `window.HG` — the host-facing contract (`platform/tech-spec.md` § Embeddable
3
+ * Script). Canonical source for the typings customers install via
4
+ * `@herazgo/sdk` (this package); the loader compiles its own sub-API against
5
+ * `HGModuleApi` (`satisfies` in `src/host-api.ts` of the sdk repo), so a
6
+ * loader change that drifts from the published contract fails typecheck.
7
+ *
8
+ * One key per enabled module; a disabled module has no key at all. Hosts
9
+ * pre-seed `window.HG = { <module>: { hidden: true } }` before the snippet
10
+ * loads — through `seedHG()` rather than this type (the seed grammar is
11
+ * wider than the upgraded API).
12
+ */
13
+ /** One module's sub-API on `HG.<module>`, exposed after the loader upgrade. */
14
+ export interface HGModuleApi {
15
+ /** Host-led hidden state (seed / `hide()`); presentation rules own the launcher display only. */
16
+ readonly hidden: boolean;
17
+ /** Live panel state; false while the module is unmounted. */
18
+ readonly opened: boolean;
19
+ /** Kill switch: unmounts instantly, mid-flight mounts included. */
20
+ hide(): void;
21
+ /** Re-mount (booting first when the module was seed-hidden). */
22
+ show(): void;
23
+ /** Open the panel: mounts if needed, ignoring rules and the hidden state. */
24
+ open(): void;
25
+ /** Close the panel; visibility returns to the rules/host state. */
26
+ close(): void;
27
+ /** Panel open/close events; returns the unsubscribe. */
28
+ on(type: "open" | "close", cb: () => void): () => void;
29
+ }
30
+ /** The upgraded host namespace (set once the snippet claims `window.HG`). */
31
+ export interface HGGlobal {
32
+ /** Marker the loader sets when it owns the name. */
33
+ __herazgo: true;
34
+ /** One entry per enabled product module (feedback today; extended as modules ship). */
35
+ feedback?: HGModuleApi;
36
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `window.HG` — the host-facing contract (`platform/tech-spec.md` § Embeddable
3
+ * Script). Canonical source for the typings customers install via
4
+ * `@herazgo/sdk` (this package); the loader compiles its own sub-API against
5
+ * `HGModuleApi` (`satisfies` in `src/host-api.ts` of the sdk repo), so a
6
+ * loader change that drifts from the published contract fails typecheck.
7
+ *
8
+ * One key per enabled module; a disabled module has no key at all. Hosts
9
+ * pre-seed `window.HG = { <module>: { hidden: true } }` before the snippet
10
+ * loads — through `seedHG()` rather than this type (the seed grammar is
11
+ * wider than the upgraded API).
12
+ */
13
+ export {};
package/lib/index.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { loadHG, seedHG } from "./load-hg.js";
2
+ export type { HGSeedEntry, LoadHGOptions } from "./load-hg.js";
3
+ import type { HGGlobal } from "./host-api-types.js";
4
+ export type { HGGlobal, HGModuleApi } from "./host-api-types.js";
5
+ import HG from "./facade.js";
6
+ export type { HGFacade } from "./facade.js";
7
+ export { createHG } from "./facade.js";
8
+ export default HG;
9
+ declare global {
10
+ interface Window {
11
+ /** Herazgo host API, set by the embeddable script (hg.js). */
12
+ HG?: HGGlobal | undefined;
13
+ }
14
+ }
package/lib/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { loadHG, seedHG } from "./load-hg.js";
2
+ import HG from "./facade.js";
3
+ export { createHG } from "./facade.js";
4
+ export default HG;
@@ -0,0 +1,18 @@
1
+ /** Pre-seed entry: first-render suppression before the snippet loads. */
2
+ export interface HGSeedEntry {
3
+ hidden?: boolean;
4
+ }
5
+ export interface LoadHGOptions {
6
+ /** Product public key from the studio snippet (`hz-…`). */
7
+ key: string;
8
+ /** Wait for this module's sub-API (e.g. "feedback"); default: the loader marker. */
9
+ module?: string;
10
+ /** Snippet URL override (dev/self-host); default: the prod CDN. */
11
+ scriptUrl?: string;
12
+ /** Ready-wait budget in ms; default 10000. Non-positive: check once, no wait. */
13
+ timeoutMs?: number;
14
+ }
15
+ /** Typed pre-seed for `window.HG` — call before the snippet / loadHG(). */
16
+ export declare function seedHG(modules: Record<string, HGSeedEntry>): void;
17
+ /** Injects the snippet for `key` and resolves when the widget is usable. */
18
+ export declare function loadHG(options: LoadHGOptions): Promise<void>;
package/lib/load-hg.js ADDED
@@ -0,0 +1,84 @@
1
+ const DEFAULT_SCRIPT_URL = "https://static.herazgo.com/hg.js";
2
+ /** Mirrors the loader's key check (loader.ts main()). */
3
+ const KEY_PATTERN = /^[\w-]{3,64}$/;
4
+ /** Typed pre-seed for `window.HG` — call before the snippet / loadHG(). */
5
+ export function seedHG(modules) {
6
+ if (typeof window === "undefined") {
7
+ throw new Error("seedHG: browser-only; there is no window here");
8
+ }
9
+ const seed = {};
10
+ for (const [name, entry] of Object.entries(modules)) {
11
+ seed[name] = { hidden: entry.hidden === true };
12
+ }
13
+ window.HG = seed;
14
+ }
15
+ /** Injects the snippet for `key` and resolves when the widget is usable. */
16
+ export function loadHG(options) {
17
+ if (typeof window === "undefined") {
18
+ return Promise.reject(new Error("loadHG: browser-only; there is no window here"));
19
+ }
20
+ if (!KEY_PATTERN.test(options.key)) {
21
+ return Promise.reject(new Error(`loadHG: key "${options.key}" is not a usable product key (3-64 word chars)`));
22
+ }
23
+ const w = window;
24
+ const scriptUrl = options.scriptUrl ?? DEFAULT_SCRIPT_URL;
25
+ const timeoutMs = options.timeoutMs ?? 10000;
26
+ const module = options.module;
27
+ return new Promise((resolve, reject) => {
28
+ let settled = false;
29
+ let poll;
30
+ let deadline;
31
+ const finish = (err) => {
32
+ if (settled)
33
+ return;
34
+ settled = true;
35
+ if (poll != null)
36
+ clearInterval(poll);
37
+ if (deadline != null)
38
+ clearTimeout(deadline);
39
+ if (err != null)
40
+ reject(err);
41
+ else
42
+ resolve();
43
+ };
44
+ const ready = () => {
45
+ if (module != null) {
46
+ // Sub-APIs exist only for enabled modules after boot; a wrong or
47
+ // disabled module name times out (the error says which).
48
+ return w.HG != null && w.HG[module] != null;
49
+ }
50
+ return w.HG?.__herazgo === true;
51
+ };
52
+ if (ready())
53
+ return finish();
54
+ const src = `${scriptUrl}?key=${encodeURIComponent(options.key)}`;
55
+ // Idempotent per key: a snippet (with or without our marker attr) or an
56
+ // earlier loadHG() may already be loading; the loader dedupes via
57
+ // window.__herazgoLoaded regardless. A pre-existing tagged snippet shares
58
+ // its load error with this caller too.
59
+ const existing = document.querySelector(`script[data-hg-key="${options.key}"]`);
60
+ if (existing == null) {
61
+ const snippet = document.createElement("script");
62
+ snippet.src = src;
63
+ snippet.async = true;
64
+ snippet.dataset.hgKey = options.key;
65
+ snippet.addEventListener("error", () => {
66
+ finish(new Error(`loadHG: failed to load ${src} (blocked or unreachable)`));
67
+ });
68
+ document.head.appendChild(snippet);
69
+ }
70
+ else {
71
+ existing.addEventListener("error", () => {
72
+ finish(new Error(`loadHG: failed to load ${src} (blocked or unreachable)`));
73
+ });
74
+ }
75
+ poll = setInterval(() => {
76
+ if (ready())
77
+ finish();
78
+ }, 50);
79
+ deadline = setTimeout(() => {
80
+ finish(new Error(`loadHG: timed out after ${timeoutMs}ms waiting for ${module ?? "the loader"} — ` +
81
+ `check the key${module != null ? `, that "${module}" is enabled` : ", network reachability"}`));
82
+ }, Math.max(0, timeoutMs));
83
+ });
84
+ }
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@herazgo/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Typings and install helpers for the Herazgo embeddable script (hg.js). The widget runtime always loads from the Herazgo CDN.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://gitlab.com/herazgo/sdk.git",
10
+ "directory": "packages/sdk"
11
+ },
12
+ "keywords": [
13
+ "herazgo",
14
+ "feedback",
15
+ "widget",
16
+ "embeddable",
17
+ "sdk"
18
+ ],
19
+ "main": "./lib/index.js",
20
+ "types": "./lib/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./lib/index.d.ts",
24
+ "default": "./lib/index.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "lib"
29
+ ],
30
+ "devDependencies": {
31
+ "typescript": "^7.0.2"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.json",
35
+ "typecheck": "tsc --noEmit -p tsconfig.json"
36
+ }
6
37
  }