@ethisyscore/plugin-ui 1.47.0 → 1.49.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/index.cjs CHANGED
@@ -20,7 +20,7 @@ function useCurrentUser() {
20
20
  }
21
21
  var CONTEXT_KEY = /* @__PURE__ */ Symbol.for("ethisyscore.plugin-ui.hostSidebarActionsContext");
22
22
  var SINK_KEY = /* @__PURE__ */ Symbol.for("ethisyscore.plugin-ui.hostChromeDiagnosticSink");
23
- var HOST_CHROME_CONTRACT_VERSION = "2.0.0";
23
+ var HOST_CHROME_CONTRACT_VERSION = "2.1.0";
24
24
  var _g = globalThis;
25
25
  var HostSidebarActionsContext = _g[CONTEXT_KEY] ?? (_g[CONTEXT_KEY] = react.createContext(null));
26
26
  var _gs = globalThis;
@@ -53,8 +53,17 @@ function toDescriptor(a) {
53
53
  const { onSelect: _drop, ...rest } = a;
54
54
  return rest;
55
55
  }
56
+ if (a.kind === "group") {
57
+ const { children, ...rest } = a;
58
+ return { ...rest, children: children.map(toDescriptor) };
59
+ }
56
60
  return a;
57
61
  }
62
+ function collectDispatchHandlers(actions) {
63
+ return actions.flatMap(
64
+ (a) => a.kind === "dispatch" && a.onSelect ? [[a.id, a.onSelect]] : a.kind === "group" ? collectDispatchHandlers(a.children) : []
65
+ );
66
+ }
58
67
  function toContribution(input) {
59
68
  if (input.mode === SIDEBAR_MODE.none) {
60
69
  return { mode: SIDEBAR_MODE.none };
@@ -81,7 +90,7 @@ function contributionKey(input) {
81
90
  if (input.mode === SIDEBAR_MODE.none) {
82
91
  return JSON.stringify([SIDEBAR_MODE.none]);
83
92
  }
84
- const actionTuples = input.actions.map((a) => [
93
+ const actionTuple = (a) => [
85
94
  a.id,
86
95
  a.label,
87
96
  a.icon ?? "",
@@ -91,8 +100,11 @@ function contributionKey(input) {
91
100
  a.active ? 1 : 0,
92
101
  a.requiredPermission ?? "",
93
102
  a.kind,
94
- a.kind === "navigate" ? a.href : ""
95
- ]);
103
+ a.kind === "navigate" ? a.href : "",
104
+ // Group children fold into the key so a change to any collapsed leaf forces a republish.
105
+ a.kind === "group" ? a.children.map(actionTuple) : ""
106
+ ];
107
+ const actionTuples = input.actions.map(actionTuple);
96
108
  if (input.mode === SIDEBAR_MODE.actions) {
97
109
  return JSON.stringify([SIDEBAR_MODE.actions, input.entityToken, actionTuples]);
98
110
  }
@@ -125,11 +137,7 @@ function useHostSidebarActions(input) {
125
137
  const compatible = api && isHostChromeCompatible(api.capabilities?.contractVersion) ? api : null;
126
138
  const actions = input.mode === SIDEBAR_MODE.none ? [] : input.actions;
127
139
  const handlers = react.useRef(/* @__PURE__ */ new Map());
128
- handlers.current = new Map(
129
- actions.flatMap(
130
- (a) => a.kind === "dispatch" && a.onSelect ? [[a.id, a.onSelect]] : []
131
- )
132
- );
140
+ handlers.current = new Map(collectDispatchHandlers(actions));
133
141
  react.useEffect(() => {
134
142
  if (!compatible) {
135
143
  reportNoHost(pageId);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/currentUser.ts","../src/platform-react/hostSidebarActionsContext.ts","../src/platform-react/surfaceUrl.ts","../src/platform-react/useHostSidebarActions.ts"],"names":["useHostIdentity","createContext","useContext","useRef","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `useCurrentUser` — the display-only current-user hook for plugin frontends\n * (WI 5154 follow-up #2, plugin-ui host-identity).\n *\n * A plugin page often needs the signed-in user for UX bits — a \"you are signed\n * in as …\" hint, pre-selecting the caller in a sign-off panel, or a\n * \"my acknowledgements\" heading. This hook surfaces that identity from the\n * host without the plugin importing the lower-level {@link useHostIdentity}\n * seam directly.\n *\n * It is a thin adapter over the host-provided {@link HostIdentity} context\n * (bound by the host through `ExtensionRuntimeProvider`'s `identity` prop):\n * it projects the host's richer {@link HostIdentityUser} down to the minimal\n * `{ id, displayName }` shape a plugin page needs for display.\n *\n * ⚠️ SECURITY — DISPLAY ONLY. This is presentation/UX data, NOT an\n * authorization source. Authorization is enforced HOST-SIDE at the MCP\n * boundary: the plugin backend re-authorises every tool/resource call from the\n * trusted server session. Never gate a mutation or a data read on this value.\n *\n * Returns `null` when the host has not provided an identity — standalone / mock\n * hosts, a host that predates the identity seam, or while host auth is still\n * loading or the caller is unauthenticated. Callers MUST handle `null`\n * (this matches the pre-existing plugin `useAuth().user` shim, which returned\n * `undefined`, so adopting this hook is a no-regression change).\n */\nimport { useHostIdentity } from \"@ethisyscore/extension-runtime/plugin\";\n\n/**\n * Minimal current-user shape a plugin page reads for display. Mirrors the\n * identity the host authenticates: a stable `id` and a human-readable\n * `displayName`. (No email — the host identity seam does not currently\n * forward one; if that changes, extend {@link HostIdentityUser} first and\n * project it here.)\n */\nexport interface CurrentUser {\n /** Stable user id (matches the host's authenticated caller id). */\n id: string;\n /** Human-readable display name (the host's full name). */\n displayName: string;\n}\n\n/**\n * Returns the host-authenticated current user projected to the display-only\n * {@link CurrentUser} shape, or `null` when no host identity is available.\n *\n * Display-only — see the module doc: authorization stays host-enforced at the\n * MCP boundary. Do NOT use the returned value as an authorization decision.\n */\nexport function useCurrentUser(): CurrentUser | null {\n const identity = useHostIdentity();\n const user = identity?.user ?? null;\n if (!user) {\n return null;\n }\n return {\n id: user.id,\n // The host binds `fullName`; fall back to composing first + last so a host\n // that only sets the parts still yields a usable display name.\n displayName:\n user.fullName?.trim() || `${user.firstName ?? \"\"} ${user.lastName ?? \"\"}`.trim(),\n };\n}\n","import { createContext } from \"react\";\nimport type { Context } from \"react\";\nimport type { HostSidebarActionsApiShape } from \"@ethisyscore/components-react\";\n\n// ── Cross-bundle singleton keys ───────────────────────────────────────────────\n// tsup may inline this module into BOTH the package-root entry and the subpath\n// entry, producing two distinct module copies. Storing shared state on globalThis\n// under Symbol.for(…) keys guarantees that every copy resolves the SAME object.\nconst CONTEXT_KEY = Symbol.for(\"ethisyscore.plugin-ui.hostSidebarActionsContext\");\nconst SINK_KEY = Symbol.for(\"ethisyscore.plugin-ui.hostChromeDiagnosticSink\");\n\n/** Host-chrome / sidebar-actions contract version (semver). Host MAJOR must equal the SDK\n * MAJOR before any publish/subscribe; otherwise the plugin hook fails closed (WI 5188 batch 2). */\nexport const HOST_CHROME_CONTRACT_VERSION = \"2.0.0\";\n\nexport type HostSidebarActionSlot = \"primary\" | \"overflow\" | \"sidebar-only\";\nexport type HostSidebarActionVariant = \"default\" | \"danger\";\n\n/** Fields common to every action. */\ninterface HostSidebarActionBase {\n id: string;\n label: string;\n icon?: string;\n slot?: HostSidebarActionSlot;\n variant?: HostSidebarActionVariant;\n disabled?: boolean;\n active?: boolean;\n /** Optional host-side double-gate: the mask is resolved against the surface extension groupCode. */\n requiredPermission?: number;\n}\n\n/** Strictly serializable action the plugin publishes to the host sidebar — NO functions cross\n * the boundary. Discriminated union so `href` is REQUIRED at the type level for a navigate\n * action and FORBIDDEN for a dispatch action (spec-gate MEDIUM R1-M1 — a navigate with a\n * missing href fails typecheck at authoring time rather than being silently dropped by the host).\n * `kind:\"dispatch\"` is clicked back to the plugin by `id` via the action-emit subscription. */\nexport type HostSidebarActionDescriptor =\n | (HostSidebarActionBase & { kind: \"dispatch\" })\n | (HostSidebarActionBase & { kind: \"navigate\"; href: string }); // extension-relative; host normalizes/validates\n\n/** A navigation leaf the plugin drives into the host detail sidebar. `href` is extension-relative;\n * the host canonicalises + confines it to `/extensions/<slug>/…` (host-owned, never trusted). */\nexport interface HostSidebarNavItem {\n id: string;\n label: string;\n icon?: string;\n href: string;\n}\n\n/** The single publish payload. `none` = contribute nothing (do NOT clobber the static manifest\n * actions); `actions` = batch-2 Quick Actions for a detail entity; `detail` = full detail-nav\n * REPLACEMENT (title + nav items + back-arrow + Quick Actions). `identityEpoch` is host-stamped\n * (plan D-1), so it is not part of this plugin-published shape. */\nexport type HostSidebarContribution =\n | { mode: \"none\" }\n | { mode: \"actions\"; entityToken: string; actions: HostSidebarActionDescriptor[] }\n | {\n mode: \"detail\";\n entityToken: string;\n title: string;\n navSectionLabel: string;\n navItems: HostSidebarNavItem[];\n backHref: string;\n backLabel: string;\n actions: HostSidebarActionDescriptor[];\n };\n\nexport interface HostSidebarActionsCapabilities {\n readonly contractVersion: string; // HOST_CHROME_CONTRACT_VERSION the host implements\n}\n\n/** Host-provided API, passed to a PlatformReact page as the `hostSidebar` prop and read via\n * the plugin-bundled context. Additive-only. */\nexport interface HostSidebarActionsApi {\n /** Replace the surface's sidebar contribution. The host rejects a publish whose entityToken ≠\n * its current location-derived token, rejects duplicate action ids, and — for mode:\"detail\" —\n * rejects/atomically-drops an invalid or href-unsafe context (falls back to main nav). */\n publish(contribution: HostSidebarContribution): void;\n /** Revert to the static-manifest fallback (publication state = unpublished). */\n clear(): void;\n /** Subscribe to click dispatch for `kind:\"dispatch\"` actions. The host emits the clicked\n * action id. Returns an unsubscribe. The plugin bridge subscribes exactly once. */\n subscribe(onAction: (actionId: string) => void): () => void;\n readonly capabilities: HostSidebarActionsCapabilities;\n}\n\n/** Props the host injects into a page for the sidebar-actions seam (merged into PlatformReactPageProps).\n * Typed against the dependency-neutral `HostSidebarActionsApiShape` — the SAME type\n * `PlatformReactPageProps.hostSidebar` uses in `@ethisyscore/components-react` — so this\n * convenience alias never diverges from the canonical page-props declaration. The concrete\n * `HostSidebarActionsApi` is structurally assignable to the shape; the page bridge narrows to\n * it internally. */\nexport interface HostSidebarActionsPageProps {\n hostSidebar?: HostSidebarActionsApiShape;\n}\n\ntype GlobalWithCtx = typeof globalThis & {\n [CONTEXT_KEY]?: Context<HostSidebarActionsApi | null>;\n};\nconst _g = globalThis as GlobalWithCtx;\nexport const HostSidebarActionsContext: Context<HostSidebarActionsApi | null> =\n _g[CONTEXT_KEY] ?? (_g[CONTEXT_KEY] = createContext<HostSidebarActionsApi | null>(null));\n\n// ── Diagnostics adapter (concrete, testable — spec-gate HIGH R1-H3) ─────────────\n// A single injectable sink so degradation is OBSERVABLE in all environments without inventing a\n// `globalThis` global. The host runtime calls `setHostChromeDiagnosticSink` once to route these to\n// its real telemetry; absent a sink it is a safe no-op. Every emission carries the page id tag.\n//\n// The sink is stored on the SINK_KEY globalThis slot so that setHostChromeDiagnosticSink and\n// reportHostChromeDiagnostic in any duplicate module copy read/write the SAME slot.\nexport interface HostChromeDiagnostic {\n event: string;\n pageId?: string;\n detail?: unknown;\n}\ntype DiagnosticSinkFn = (d: HostChromeDiagnostic) => void;\ntype GlobalWithSink = typeof globalThis & { [SINK_KEY]?: DiagnosticSinkFn };\nconst _gs = globalThis as GlobalWithSink;\nif (_gs[SINK_KEY] === undefined) {\n _gs[SINK_KEY] = () => {};\n}\nexport function setHostChromeDiagnosticSink(sink: DiagnosticSinkFn): void {\n _gs[SINK_KEY] = sink;\n}\nexport function reportHostChromeDiagnostic(d: HostChromeDiagnostic): void {\n try {\n _gs[SINK_KEY]!(d);\n } catch {\n /* diagnostics are best-effort — never throw into the plugin */\n }\n}\n\n/** Major-equality compatibility check; fail-closed on absent/unparseable input. */\nexport function isHostChromeCompatible(hostVersion: string | undefined): boolean {\n if (hostVersion === undefined) return false;\n const hostMajor = Number.parseInt(hostVersion.split(\".\")[0] ?? \"\", 10);\n const sdkMajor = Number.parseInt(HOST_CHROME_CONTRACT_VERSION.split(\".\")[0] ?? \"\", 10);\n if (Number.isNaN(hostMajor) || Number.isNaN(sdkMajor)) return false;\n return hostMajor === sdkMajor;\n}\n","import { createContext, useContext } from \"react\";\nimport { useHostIdentity } from \"@ethisyscore/extension-runtime/plugin\";\n\n/** The surface's mount context, provided by definePlatformReactPluginPage. */\nexport interface SurfaceBaseValue {\n /** Host-provided mount path `/extensions/<slug>/<pageId>` (authoritative). */\n basePath?: string;\n /** The page id (from PlatformReactPageProps), used for the derived fallback. */\n pageId?: string;\n}\n\nexport const SurfaceBaseContext = createContext<SurfaceBaseValue | undefined>(undefined);\n\n/** Match the host's slug normalisation (useExtensionSurfaceShellPage `normaliseSlug`). */\nexport function normaliseSlug(value: string): string {\n return value.trim().toLowerCase();\n}\n\nfunction stripTrailingSlash(p: string): string {\n return p.length > 1 && p.endsWith(\"/\") ? p.slice(0, -1) : p;\n}\n\n/**\n * Resolve the surface base. Precedence: host-provided basePath → derived from\n * normaliseSlug(extensionGroupCode) + pageId → null (caller decides how to fail).\n * Pure — callers read context + identity and pass the pieces in.\n */\nexport function resolveSurfaceBase(input: {\n basePath?: string;\n pageId?: string;\n extensionGroupCode?: string | null;\n}): { base: string; groupRoot: string } | null {\n if (input.basePath) {\n const base = stripTrailingSlash(input.basePath);\n const cut = base.lastIndexOf(\"/\");\n const groupRoot = cut > 0 ? base.slice(0, cut) : base;\n return { base, groupRoot };\n }\n const slug = input.extensionGroupCode ? normaliseSlug(input.extensionGroupCode) : \"\";\n const pageId = input.pageId ?? \"\";\n if (slug && pageId) {\n const groupRoot = `/extensions/${slug}`;\n return { base: `${groupRoot}/${pageId}`, groupRoot };\n }\n return null;\n}\n\n/** Join base + sub (leading slash stripped) + optional ?query/#hash suffix. */\nexport function buildSurfaceUrl(base: string, sub = \"\", suffix = \"\"): string {\n const b = stripTrailingSlash(base);\n const s = sub.replace(/^\\/+/, \"\");\n const path = s ? `${b}/${s}` : b;\n return suffix ? `${path}${suffix}` : path;\n}\n\n/**\n * Hook: build a URL relative to the CURRENT surface. Precedence per\n * resolveSurfaceBase; throws in dev when unresolved (never returns app-root).\n */\nexport function useSurfaceUrl(): (sub?: string, suffix?: string) => string {\n const ctx = useContext(SurfaceBaseContext);\n const identity = useHostIdentity();\n return (sub = \"\", suffix = \"\") => {\n const resolved = resolveSurfaceBase({\n basePath: ctx?.basePath,\n pageId: ctx?.pageId,\n extensionGroupCode: identity?.extensionGroupCode ?? null,\n });\n if (!resolved) {\n const msg =\n \"useSurfaceUrl: no surface base — SurfaceBaseContext (basePath/pageId) and \" +\n \"extensionGroupCode are both unavailable. Ensure the page is wrapped by \" +\n \"definePlatformReactPluginPage inside the host runtime.\";\n // Dev: fail loud. Prod: never navigate to app-root (that recreates the 404\n // class) — stay on the current path (a no-op) so a transient unresolved\n // state (e.g. identity still loading on a pre-basePath host) can't crash the\n // surface. Callers invoke this in event handlers, by which time identity has\n // loaded and the base resolves normally.\n // Guard `process` access: in a browser/ESM consumer where `process` is\n // undefined, reading `process.env.NODE_ENV` directly would throw a\n // ReferenceError before the prod fallback runs. `typeof` never throws on an\n // undeclared identifier. Fail SAFE — throw ONLY when we can positively\n // confirm a non-production env; an unknown env (no `process`) is treated as\n // production so a bundled surface never crashes here.\n const isDevEnv =\n typeof process !== \"undefined\" && process.env?.NODE_ENV !== \"production\";\n if (isDevEnv) throw new Error(msg);\n if (typeof console !== \"undefined\") console.error(msg);\n // Prod no-op: stay on the FULL current URL (path + query + hash) — a true\n // no-op, and never app-root.\n return typeof window !== \"undefined\"\n ? window.location.pathname + window.location.search + window.location.hash\n : \"\";\n }\n return buildSurfaceUrl(resolved.base, sub, suffix);\n };\n}\n\n/** Pure: build a URL for ANOTHER surface (cross-surface, e.g. an overlay opening a page). */\nexport function surfacePathFor(opts: {\n slug: string;\n pageId: string;\n sub?: string;\n suffix?: string;\n}): string {\n const base = `/extensions/${normaliseSlug(opts.slug)}/${opts.pageId}`;\n return buildSurfaceUrl(base, opts.sub ?? \"\", opts.suffix ?? \"\");\n}\n","import { useContext, useEffect, useRef } from \"react\";\nimport {\n HostSidebarActionsContext,\n isHostChromeCompatible,\n reportHostChromeDiagnostic,\n type HostSidebarActionDescriptor,\n type HostSidebarActionsApi,\n type HostSidebarContribution,\n type HostSidebarNavItem,\n} from \"./hostSidebarActionsContext\";\nimport { SurfaceBaseContext } from \"./surfaceUrl\";\n\n/** Authoring shape: a descriptor plus — ONLY for `kind:\"dispatch\"` — an inline handler.\n * A distributive union (not a blanket intersection over the whole descriptor union): `onSelect`\n * is permitted only on a dispatch action. A navigate action carries no handler (the host performs\n * the navigation from `href`), so supplying `onSelect` on a navigate action is a typecheck error\n * rather than a silently-ignored footgun. The hook strips `onSelect` before publishing (only\n * serializable descriptors cross to the host). */\nexport type HostSidebarAction =\n | (Extract<HostSidebarActionDescriptor, { kind: \"dispatch\" }> & { onSelect?: () => void })\n | Extract<HostSidebarActionDescriptor, { kind: \"navigate\" }>;\n\n/** Single source for the discriminant values used across the mode checks, `toContribution`, and\n * the content key — so the \"none\"/\"actions\"/\"detail\" strings live in one place. The union type\n * below intentionally keeps its literal members (they ARE the discriminant + drive IntelliSense);\n * `SIDEBAR_MODE.*` is `as const`, so it is type-identical to those literals. */\nexport const SIDEBAR_MODE = { none: \"none\", actions: \"actions\", detail: \"detail\" } as const;\n\n/** Discriminated input to `useHostSidebarActions`. `mode:\"none\"` suppresses all plugin-side\n * contributions without clobbering the static manifest actions. `mode:\"actions\"` is the\n * batch-2 Quick Actions shape. `mode:\"detail\"` is the full detail-nav replacement with title,\n * navigation items, back-arrow, and Quick Actions. */\nexport type HostSidebarInput =\n | { mode: \"none\" }\n | { mode: \"actions\"; entityToken: string; actions: HostSidebarAction[] }\n | {\n mode: \"detail\";\n entityToken: string;\n title: string;\n navSectionLabel: string;\n navItems: HostSidebarNavItem[];\n backHref: string;\n backLabel: string;\n actions: HostSidebarAction[];\n };\n\nfunction toDescriptor(a: HostSidebarAction): HostSidebarActionDescriptor {\n if (a.kind === \"dispatch\") {\n const { onSelect: _drop, ...rest } = a;\n return rest; // rest is the serializable dispatch descriptor\n }\n return a; // navigate descriptor is already serializable (no handler)\n}\n\n/** Map a plugin-authoring input to the serializable contribution published to the host.\n * `onSelect` handlers are stripped via `toDescriptor`; nav/back/title pass through for `detail`. */\nfunction toContribution(input: HostSidebarInput): HostSidebarContribution {\n if (input.mode === SIDEBAR_MODE.none) {\n return { mode: SIDEBAR_MODE.none };\n }\n if (input.mode === SIDEBAR_MODE.actions) {\n return {\n mode: SIDEBAR_MODE.actions,\n entityToken: input.entityToken,\n actions: input.actions.map(toDescriptor),\n };\n }\n // mode === \"detail\"\n return {\n mode: SIDEBAR_MODE.detail,\n entityToken: input.entityToken,\n title: input.title,\n navSectionLabel: input.navSectionLabel,\n navItems: input.navItems,\n backHref: input.backHref,\n backLabel: input.backLabel,\n actions: input.actions.map(toDescriptor),\n };\n}\n\n/** Content key so we republish only when the visible descriptor set changes. JSON-encodes an\n * ARRAY of ORDERED-tuple arrays: element order is stable (unlike object property insertion order,\n * which is why we avoid `JSON.stringify` over the raw descriptor objects — spec-gate MEDIUM R1-M2),\n * and JSON string-escaping makes the key delimiter-collision-proof, so a `label`/`href` that\n * happens to contain a separator character can no longer alias two distinct action sets to the\n * same key (which would skip a needed republish and leave the host sidebar stale). */\nfunction contributionKey(input: HostSidebarInput): string {\n if (input.mode === SIDEBAR_MODE.none) {\n return JSON.stringify([SIDEBAR_MODE.none]);\n }\n const actionTuples = input.actions.map((a) => [\n a.id,\n a.label,\n a.icon ?? \"\",\n a.slot ?? \"\",\n a.variant ?? \"\",\n a.disabled ? 1 : 0,\n a.active ? 1 : 0,\n a.requiredPermission ?? \"\",\n a.kind,\n a.kind === \"navigate\" ? a.href : \"\",\n ]);\n if (input.mode === SIDEBAR_MODE.actions) {\n return JSON.stringify([SIDEBAR_MODE.actions, input.entityToken, actionTuples]);\n }\n // mode === \"detail\"\n const navTuples = input.navItems.map((n) => [n.id, n.label, n.icon ?? \"\", n.href]);\n return JSON.stringify([\n SIDEBAR_MODE.detail,\n input.entityToken,\n input.title,\n input.navSectionLabel,\n input.backHref,\n input.backLabel,\n navTuples,\n actionTuples,\n ]);\n}\n\n/** Per-page dev-warn throttle (NOT a global boolean — a module singleton would suppress warnings\n * for every later surface in a shared bundle; spec-gate HIGH R1-H2). Telemetry fires every time. */\nconst devWarnedPages = new Set<string>();\nfunction reportNoHost(pageId: string | undefined): void {\n // Observable in ALL environments via the injected diagnostic sink, tagged by page id (R1-H3).\n reportHostChromeDiagnostic({ event: \"host_sidebar_actions_unavailable\", pageId });\n const key = pageId ?? \"*\";\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((import.meta as any).env?.DEV && !devWarnedPages.has(key)) {\n devWarnedPages.add(key);\n // eslint-disable-next-line no-console\n console.warn(\n `[plugin-ui] useHostSidebarActions (${key}): no compatible host sidebar-actions API — ` +\n \"the host predates the seam or its contract major differs. Quick Actions will not render.\",\n );\n }\n}\n\n/**\n * Publish contextual sidebar contributions from a PlatformReact page to the host sidebar, and\n * receive click dispatch by id. Accepts a discriminated `HostSidebarInput`:\n * - `mode:\"none\"` — suppress all plugin contributions without clobbering the static manifest.\n * - `mode:\"actions\"` — batch-2 Quick Actions for a detail entity (entity token + action list).\n * - `mode:\"detail\"` — full detail-nav replacement (title, nav items, back-arrow, Quick Actions).\n *\n * Serializable descriptors only cross to the host; the inline `onSelect` handlers stay in a\n * ref-map here and are always the latest closure. Fails closed (no publish/subscribe, no throw)\n * when the host predates or is incompatible with the seam. */\nexport function useHostSidebarActions(input: HostSidebarInput): void {\n const api = useContext(HostSidebarActionsContext);\n const pageId = useContext(SurfaceBaseContext)?.pageId; // diagnostic tag (per-surface)\n const compatible: HostSidebarActionsApi | null =\n api && isHostChromeCompatible(api.capabilities?.contractVersion) ? api : null;\n\n // Ref-map refreshed every render → dispatch always hits the latest handler.\n const actions = input.mode === SIDEBAR_MODE.none ? [] : input.actions;\n const handlers = useRef<Map<string, () => void>>(new Map());\n handlers.current = new Map(\n actions.flatMap((a) =>\n a.kind === \"dispatch\" && a.onSelect ? [[a.id, a.onSelect] as const] : [],\n ),\n );\n\n useEffect(() => {\n if (!compatible) {\n reportNoHost(pageId);\n return;\n }\n const unsub = compatible.subscribe((id) => {\n handlers.current.get(id)?.();\n });\n return () => {\n compatible.clear();\n unsub();\n };\n }, [compatible, pageId]);\n\n // Content key: mode + entityToken + descriptor tuples + (detail) nav/back/title.\n const key = contributionKey(input);\n useEffect(() => {\n if (!compatible) return;\n compatible.publish(toContribution(input));\n // key encodes all content that affects the published contribution; changes force republish.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [compatible, key]);\n}\n"]}
1
+ {"version":3,"sources":["../src/currentUser.ts","../src/platform-react/hostSidebarActionsContext.ts","../src/platform-react/surfaceUrl.ts","../src/platform-react/useHostSidebarActions.ts"],"names":["useHostIdentity","createContext","useContext","useRef","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `useCurrentUser` — the display-only current-user hook for plugin frontends\n * (WI 5154 follow-up #2, plugin-ui host-identity).\n *\n * A plugin page often needs the signed-in user for UX bits — a \"you are signed\n * in as …\" hint, pre-selecting the caller in a sign-off panel, or a\n * \"my acknowledgements\" heading. This hook surfaces that identity from the\n * host without the plugin importing the lower-level {@link useHostIdentity}\n * seam directly.\n *\n * It is a thin adapter over the host-provided {@link HostIdentity} context\n * (bound by the host through `ExtensionRuntimeProvider`'s `identity` prop):\n * it projects the host's richer {@link HostIdentityUser} down to the minimal\n * `{ id, displayName }` shape a plugin page needs for display.\n *\n * ⚠️ SECURITY — DISPLAY ONLY. This is presentation/UX data, NOT an\n * authorization source. Authorization is enforced HOST-SIDE at the MCP\n * boundary: the plugin backend re-authorises every tool/resource call from the\n * trusted server session. Never gate a mutation or a data read on this value.\n *\n * Returns `null` when the host has not provided an identity — standalone / mock\n * hosts, a host that predates the identity seam, or while host auth is still\n * loading or the caller is unauthenticated. Callers MUST handle `null`\n * (this matches the pre-existing plugin `useAuth().user` shim, which returned\n * `undefined`, so adopting this hook is a no-regression change).\n */\nimport { useHostIdentity } from \"@ethisyscore/extension-runtime/plugin\";\n\n/**\n * Minimal current-user shape a plugin page reads for display. Mirrors the\n * identity the host authenticates: a stable `id` and a human-readable\n * `displayName`. (No email — the host identity seam does not currently\n * forward one; if that changes, extend {@link HostIdentityUser} first and\n * project it here.)\n */\nexport interface CurrentUser {\n /** Stable user id (matches the host's authenticated caller id). */\n id: string;\n /** Human-readable display name (the host's full name). */\n displayName: string;\n}\n\n/**\n * Returns the host-authenticated current user projected to the display-only\n * {@link CurrentUser} shape, or `null` when no host identity is available.\n *\n * Display-only — see the module doc: authorization stays host-enforced at the\n * MCP boundary. Do NOT use the returned value as an authorization decision.\n */\nexport function useCurrentUser(): CurrentUser | null {\n const identity = useHostIdentity();\n const user = identity?.user ?? null;\n if (!user) {\n return null;\n }\n return {\n id: user.id,\n // The host binds `fullName`; fall back to composing first + last so a host\n // that only sets the parts still yields a usable display name.\n displayName:\n user.fullName?.trim() || `${user.firstName ?? \"\"} ${user.lastName ?? \"\"}`.trim(),\n };\n}\n","import { createContext } from \"react\";\nimport type { Context } from \"react\";\nimport type { HostSidebarActionsApiShape } from \"@ethisyscore/components-react\";\n\n// ── Cross-bundle singleton keys ───────────────────────────────────────────────\n// tsup may inline this module into BOTH the package-root entry and the subpath\n// entry, producing two distinct module copies. Storing shared state on globalThis\n// under Symbol.for(…) keys guarantees that every copy resolves the SAME object.\nconst CONTEXT_KEY = Symbol.for(\"ethisyscore.plugin-ui.hostSidebarActionsContext\");\nconst SINK_KEY = Symbol.for(\"ethisyscore.plugin-ui.hostChromeDiagnosticSink\");\n\n/** Host-chrome / sidebar-actions contract version (semver). Host MAJOR must equal the SDK\n * MAJOR before any publish/subscribe; otherwise the plugin hook fails closed (WI 5188 batch 2).\n * 2.1.0 added the additive `kind:\"group\"` action variant (collapsible Lifecycle/Share); the\n * major-equality gate keeps 2.0 and 2.1 mutually compatible (older hosts ignore group children). */\nexport const HOST_CHROME_CONTRACT_VERSION = \"2.1.0\";\n\nexport type HostSidebarActionSlot = \"primary\" | \"overflow\" | \"sidebar-only\";\nexport type HostSidebarActionVariant = \"default\" | \"danger\";\n\n/** Fields common to every action. */\ninterface HostSidebarActionBase {\n id: string;\n label: string;\n icon?: string;\n slot?: HostSidebarActionSlot;\n variant?: HostSidebarActionVariant;\n disabled?: boolean;\n active?: boolean;\n /** Optional host-side double-gate: the mask is resolved against the surface extension groupCode. */\n requiredPermission?: number;\n}\n\n/** Strictly serializable action the plugin publishes to the host sidebar — NO functions cross\n * the boundary. Discriminated union so `href` is REQUIRED at the type level for a navigate\n * action and FORBIDDEN for a dispatch action (spec-gate MEDIUM R1-M1 — a navigate with a\n * missing href fails typecheck at authoring time rather than being silently dropped by the host).\n * `kind:\"dispatch\"` is clicked back to the plugin by `id` via the action-emit subscription. */\nexport type HostSidebarActionDescriptor =\n | (HostSidebarActionBase & { kind: \"dispatch\" })\n | (HostSidebarActionBase & { kind: \"navigate\"; href: string }) // extension-relative; host normalizes/validates\n // A collapsible parent (e.g. Lifecycle / Share). It carries no behaviour of its own — the host\n // renders it as an accordion and its `children` as the collapsed rows. `children` is recursive:\n // each child is a navigate/dispatch action OR a nested group.\n | (HostSidebarActionBase & { kind: \"group\"; children: HostSidebarActionDescriptor[] });\n\n/** A navigation leaf the plugin drives into the host detail sidebar. `href` is extension-relative;\n * the host canonicalises + confines it to `/extensions/<slug>/…` (host-owned, never trusted). */\nexport interface HostSidebarNavItem {\n id: string;\n label: string;\n icon?: string;\n href: string;\n}\n\n/** The single publish payload. `none` = contribute nothing (do NOT clobber the static manifest\n * actions); `actions` = batch-2 Quick Actions for a detail entity; `detail` = full detail-nav\n * REPLACEMENT (title + nav items + back-arrow + Quick Actions). `identityEpoch` is host-stamped\n * (plan D-1), so it is not part of this plugin-published shape. */\nexport type HostSidebarContribution =\n | { mode: \"none\" }\n | { mode: \"actions\"; entityToken: string; actions: HostSidebarActionDescriptor[] }\n | {\n mode: \"detail\";\n entityToken: string;\n title: string;\n navSectionLabel: string;\n navItems: HostSidebarNavItem[];\n backHref: string;\n backLabel: string;\n actions: HostSidebarActionDescriptor[];\n };\n\nexport interface HostSidebarActionsCapabilities {\n readonly contractVersion: string; // HOST_CHROME_CONTRACT_VERSION the host implements\n}\n\n/** Host-provided API, passed to a PlatformReact page as the `hostSidebar` prop and read via\n * the plugin-bundled context. Additive-only. */\nexport interface HostSidebarActionsApi {\n /** Replace the surface's sidebar contribution. The host rejects a publish whose entityToken ≠\n * its current location-derived token, rejects duplicate action ids, and — for mode:\"detail\" —\n * rejects/atomically-drops an invalid or href-unsafe context (falls back to main nav). */\n publish(contribution: HostSidebarContribution): void;\n /** Revert to the static-manifest fallback (publication state = unpublished). */\n clear(): void;\n /** Subscribe to click dispatch for `kind:\"dispatch\"` actions. The host emits the clicked\n * action id. Returns an unsubscribe. The plugin bridge subscribes exactly once. */\n subscribe(onAction: (actionId: string) => void): () => void;\n readonly capabilities: HostSidebarActionsCapabilities;\n}\n\n/** Props the host injects into a page for the sidebar-actions seam (merged into PlatformReactPageProps).\n * Typed against the dependency-neutral `HostSidebarActionsApiShape` — the SAME type\n * `PlatformReactPageProps.hostSidebar` uses in `@ethisyscore/components-react` — so this\n * convenience alias never diverges from the canonical page-props declaration. The concrete\n * `HostSidebarActionsApi` is structurally assignable to the shape; the page bridge narrows to\n * it internally. */\nexport interface HostSidebarActionsPageProps {\n hostSidebar?: HostSidebarActionsApiShape;\n}\n\ntype GlobalWithCtx = typeof globalThis & {\n [CONTEXT_KEY]?: Context<HostSidebarActionsApi | null>;\n};\nconst _g = globalThis as GlobalWithCtx;\nexport const HostSidebarActionsContext: Context<HostSidebarActionsApi | null> =\n _g[CONTEXT_KEY] ?? (_g[CONTEXT_KEY] = createContext<HostSidebarActionsApi | null>(null));\n\n// ── Diagnostics adapter (concrete, testable — spec-gate HIGH R1-H3) ─────────────\n// A single injectable sink so degradation is OBSERVABLE in all environments without inventing a\n// `globalThis` global. The host runtime calls `setHostChromeDiagnosticSink` once to route these to\n// its real telemetry; absent a sink it is a safe no-op. Every emission carries the page id tag.\n//\n// The sink is stored on the SINK_KEY globalThis slot so that setHostChromeDiagnosticSink and\n// reportHostChromeDiagnostic in any duplicate module copy read/write the SAME slot.\nexport interface HostChromeDiagnostic {\n event: string;\n pageId?: string;\n detail?: unknown;\n}\ntype DiagnosticSinkFn = (d: HostChromeDiagnostic) => void;\ntype GlobalWithSink = typeof globalThis & { [SINK_KEY]?: DiagnosticSinkFn };\nconst _gs = globalThis as GlobalWithSink;\nif (_gs[SINK_KEY] === undefined) {\n _gs[SINK_KEY] = () => {};\n}\nexport function setHostChromeDiagnosticSink(sink: DiagnosticSinkFn): void {\n _gs[SINK_KEY] = sink;\n}\nexport function reportHostChromeDiagnostic(d: HostChromeDiagnostic): void {\n try {\n _gs[SINK_KEY]!(d);\n } catch {\n /* diagnostics are best-effort — never throw into the plugin */\n }\n}\n\n/** Major-equality compatibility check; fail-closed on absent/unparseable input. */\nexport function isHostChromeCompatible(hostVersion: string | undefined): boolean {\n if (hostVersion === undefined) return false;\n const hostMajor = Number.parseInt(hostVersion.split(\".\")[0] ?? \"\", 10);\n const sdkMajor = Number.parseInt(HOST_CHROME_CONTRACT_VERSION.split(\".\")[0] ?? \"\", 10);\n if (Number.isNaN(hostMajor) || Number.isNaN(sdkMajor)) return false;\n return hostMajor === sdkMajor;\n}\n","import { createContext, useContext } from \"react\";\nimport { useHostIdentity } from \"@ethisyscore/extension-runtime/plugin\";\n\n/** The surface's mount context, provided by definePlatformReactPluginPage. */\nexport interface SurfaceBaseValue {\n /** Host-provided mount path `/extensions/<slug>/<pageId>` (authoritative). */\n basePath?: string;\n /** The page id (from PlatformReactPageProps), used for the derived fallback. */\n pageId?: string;\n}\n\nexport const SurfaceBaseContext = createContext<SurfaceBaseValue | undefined>(undefined);\n\n/** Match the host's slug normalisation (useExtensionSurfaceShellPage `normaliseSlug`). */\nexport function normaliseSlug(value: string): string {\n return value.trim().toLowerCase();\n}\n\nfunction stripTrailingSlash(p: string): string {\n return p.length > 1 && p.endsWith(\"/\") ? p.slice(0, -1) : p;\n}\n\n/**\n * Resolve the surface base. Precedence: host-provided basePath → derived from\n * normaliseSlug(extensionGroupCode) + pageId → null (caller decides how to fail).\n * Pure — callers read context + identity and pass the pieces in.\n */\nexport function resolveSurfaceBase(input: {\n basePath?: string;\n pageId?: string;\n extensionGroupCode?: string | null;\n}): { base: string; groupRoot: string } | null {\n if (input.basePath) {\n const base = stripTrailingSlash(input.basePath);\n const cut = base.lastIndexOf(\"/\");\n const groupRoot = cut > 0 ? base.slice(0, cut) : base;\n return { base, groupRoot };\n }\n const slug = input.extensionGroupCode ? normaliseSlug(input.extensionGroupCode) : \"\";\n const pageId = input.pageId ?? \"\";\n if (slug && pageId) {\n const groupRoot = `/extensions/${slug}`;\n return { base: `${groupRoot}/${pageId}`, groupRoot };\n }\n return null;\n}\n\n/** Join base + sub (leading slash stripped) + optional ?query/#hash suffix. */\nexport function buildSurfaceUrl(base: string, sub = \"\", suffix = \"\"): string {\n const b = stripTrailingSlash(base);\n const s = sub.replace(/^\\/+/, \"\");\n const path = s ? `${b}/${s}` : b;\n return suffix ? `${path}${suffix}` : path;\n}\n\n/**\n * Hook: build a URL relative to the CURRENT surface. Precedence per\n * resolveSurfaceBase; throws in dev when unresolved (never returns app-root).\n */\nexport function useSurfaceUrl(): (sub?: string, suffix?: string) => string {\n const ctx = useContext(SurfaceBaseContext);\n const identity = useHostIdentity();\n return (sub = \"\", suffix = \"\") => {\n const resolved = resolveSurfaceBase({\n basePath: ctx?.basePath,\n pageId: ctx?.pageId,\n extensionGroupCode: identity?.extensionGroupCode ?? null,\n });\n if (!resolved) {\n const msg =\n \"useSurfaceUrl: no surface base — SurfaceBaseContext (basePath/pageId) and \" +\n \"extensionGroupCode are both unavailable. Ensure the page is wrapped by \" +\n \"definePlatformReactPluginPage inside the host runtime.\";\n // Dev: fail loud. Prod: never navigate to app-root (that recreates the 404\n // class) — stay on the current path (a no-op) so a transient unresolved\n // state (e.g. identity still loading on a pre-basePath host) can't crash the\n // surface. Callers invoke this in event handlers, by which time identity has\n // loaded and the base resolves normally.\n // Guard `process` access: in a browser/ESM consumer where `process` is\n // undefined, reading `process.env.NODE_ENV` directly would throw a\n // ReferenceError before the prod fallback runs. `typeof` never throws on an\n // undeclared identifier. Fail SAFE — throw ONLY when we can positively\n // confirm a non-production env; an unknown env (no `process`) is treated as\n // production so a bundled surface never crashes here.\n const isDevEnv =\n typeof process !== \"undefined\" && process.env?.NODE_ENV !== \"production\";\n if (isDevEnv) throw new Error(msg);\n if (typeof console !== \"undefined\") console.error(msg);\n // Prod no-op: stay on the FULL current URL (path + query + hash) — a true\n // no-op, and never app-root.\n return typeof window !== \"undefined\"\n ? window.location.pathname + window.location.search + window.location.hash\n : \"\";\n }\n return buildSurfaceUrl(resolved.base, sub, suffix);\n };\n}\n\n/** Pure: build a URL for ANOTHER surface (cross-surface, e.g. an overlay opening a page). */\nexport function surfacePathFor(opts: {\n slug: string;\n pageId: string;\n sub?: string;\n suffix?: string;\n}): string {\n const base = `/extensions/${normaliseSlug(opts.slug)}/${opts.pageId}`;\n return buildSurfaceUrl(base, opts.sub ?? \"\", opts.suffix ?? \"\");\n}\n","import { useContext, useEffect, useRef } from \"react\";\nimport {\n HostSidebarActionsContext,\n isHostChromeCompatible,\n reportHostChromeDiagnostic,\n type HostSidebarActionDescriptor,\n type HostSidebarActionsApi,\n type HostSidebarContribution,\n type HostSidebarNavItem,\n} from \"./hostSidebarActionsContext\";\nimport { SurfaceBaseContext } from \"./surfaceUrl\";\n\n/** Authoring shape: a descriptor plus — ONLY for `kind:\"dispatch\"` — an inline handler.\n * A distributive union (not a blanket intersection over the whole descriptor union): `onSelect`\n * is permitted only on a dispatch action. A navigate action carries no handler (the host performs\n * the navigation from `href`), so supplying `onSelect` on a navigate action is a typecheck error\n * rather than a silently-ignored footgun. The hook strips `onSelect` before publishing (only\n * serializable descriptors cross to the host). */\nexport type HostSidebarAction =\n | (Extract<HostSidebarActionDescriptor, { kind: \"dispatch\" }> & { onSelect?: () => void })\n | Extract<HostSidebarActionDescriptor, { kind: \"navigate\" }>\n // A collapsible group whose `children` are themselves authoring actions (so a dispatch child can\n // still carry its inline `onSelect`). `children` overrides the descriptor's serializable children.\n | (Omit<Extract<HostSidebarActionDescriptor, { kind: \"group\" }>, \"children\"> & { children: HostSidebarAction[] });\n\n/** Single source for the discriminant values used across the mode checks, `toContribution`, and\n * the content key — so the \"none\"/\"actions\"/\"detail\" strings live in one place. The union type\n * below intentionally keeps its literal members (they ARE the discriminant + drive IntelliSense);\n * `SIDEBAR_MODE.*` is `as const`, so it is type-identical to those literals. */\nexport const SIDEBAR_MODE = { none: \"none\", actions: \"actions\", detail: \"detail\" } as const;\n\n/** Discriminated input to `useHostSidebarActions`. `mode:\"none\"` suppresses all plugin-side\n * contributions without clobbering the static manifest actions. `mode:\"actions\"` is the\n * batch-2 Quick Actions shape. `mode:\"detail\"` is the full detail-nav replacement with title,\n * navigation items, back-arrow, and Quick Actions. */\nexport type HostSidebarInput =\n | { mode: \"none\" }\n | { mode: \"actions\"; entityToken: string; actions: HostSidebarAction[] }\n | {\n mode: \"detail\";\n entityToken: string;\n title: string;\n navSectionLabel: string;\n navItems: HostSidebarNavItem[];\n backHref: string;\n backLabel: string;\n actions: HostSidebarAction[];\n };\n\nfunction toDescriptor(a: HostSidebarAction): HostSidebarActionDescriptor {\n if (a.kind === \"dispatch\") {\n const { onSelect: _drop, ...rest } = a;\n return rest; // rest is the serializable dispatch descriptor\n }\n if (a.kind === \"group\") {\n // Recurse: strip the children's `onSelect` handlers so only serializable descriptors cross.\n const { children, ...rest } = a;\n return { ...rest, children: children.map(toDescriptor) };\n }\n return a; // navigate descriptor is already serializable (no handler)\n}\n\n/** Flatten a (possibly nested) action tree to its `kind:\"dispatch\"` handler entries — a group's\n * child dispatch clicks must reach their `onSelect` just like a top-level dispatch action. */\nfunction collectDispatchHandlers(actions: HostSidebarAction[]): Array<readonly [string, () => void]> {\n return actions.flatMap((a) =>\n a.kind === \"dispatch\" && a.onSelect\n ? [[a.id, a.onSelect] as const]\n : a.kind === \"group\"\n ? collectDispatchHandlers(a.children)\n : [],\n );\n}\n\n/** Map a plugin-authoring input to the serializable contribution published to the host.\n * `onSelect` handlers are stripped via `toDescriptor`; nav/back/title pass through for `detail`. */\nfunction toContribution(input: HostSidebarInput): HostSidebarContribution {\n if (input.mode === SIDEBAR_MODE.none) {\n return { mode: SIDEBAR_MODE.none };\n }\n if (input.mode === SIDEBAR_MODE.actions) {\n return {\n mode: SIDEBAR_MODE.actions,\n entityToken: input.entityToken,\n actions: input.actions.map(toDescriptor),\n };\n }\n // mode === \"detail\"\n return {\n mode: SIDEBAR_MODE.detail,\n entityToken: input.entityToken,\n title: input.title,\n navSectionLabel: input.navSectionLabel,\n navItems: input.navItems,\n backHref: input.backHref,\n backLabel: input.backLabel,\n actions: input.actions.map(toDescriptor),\n };\n}\n\n/** Content key so we republish only when the visible descriptor set changes. JSON-encodes an\n * ARRAY of ORDERED-tuple arrays: element order is stable (unlike object property insertion order,\n * which is why we avoid `JSON.stringify` over the raw descriptor objects — spec-gate MEDIUM R1-M2),\n * and JSON string-escaping makes the key delimiter-collision-proof, so a `label`/`href` that\n * happens to contain a separator character can no longer alias two distinct action sets to the\n * same key (which would skip a needed republish and leave the host sidebar stale). */\nfunction contributionKey(input: HostSidebarInput): string {\n if (input.mode === SIDEBAR_MODE.none) {\n return JSON.stringify([SIDEBAR_MODE.none]);\n }\n const actionTuple = (a: HostSidebarAction): unknown[] => [\n a.id,\n a.label,\n a.icon ?? \"\",\n a.slot ?? \"\",\n a.variant ?? \"\",\n a.disabled ? 1 : 0,\n a.active ? 1 : 0,\n a.requiredPermission ?? \"\",\n a.kind,\n a.kind === \"navigate\" ? a.href : \"\",\n // Group children fold into the key so a change to any collapsed leaf forces a republish.\n a.kind === \"group\" ? a.children.map(actionTuple) : \"\",\n ];\n const actionTuples = input.actions.map(actionTuple);\n if (input.mode === SIDEBAR_MODE.actions) {\n return JSON.stringify([SIDEBAR_MODE.actions, input.entityToken, actionTuples]);\n }\n // mode === \"detail\"\n const navTuples = input.navItems.map((n) => [n.id, n.label, n.icon ?? \"\", n.href]);\n return JSON.stringify([\n SIDEBAR_MODE.detail,\n input.entityToken,\n input.title,\n input.navSectionLabel,\n input.backHref,\n input.backLabel,\n navTuples,\n actionTuples,\n ]);\n}\n\n/** Per-page dev-warn throttle (NOT a global boolean — a module singleton would suppress warnings\n * for every later surface in a shared bundle; spec-gate HIGH R1-H2). Telemetry fires every time. */\nconst devWarnedPages = new Set<string>();\nfunction reportNoHost(pageId: string | undefined): void {\n // Observable in ALL environments via the injected diagnostic sink, tagged by page id (R1-H3).\n reportHostChromeDiagnostic({ event: \"host_sidebar_actions_unavailable\", pageId });\n const key = pageId ?? \"*\";\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((import.meta as any).env?.DEV && !devWarnedPages.has(key)) {\n devWarnedPages.add(key);\n // eslint-disable-next-line no-console\n console.warn(\n `[plugin-ui] useHostSidebarActions (${key}): no compatible host sidebar-actions API — ` +\n \"the host predates the seam or its contract major differs. Quick Actions will not render.\",\n );\n }\n}\n\n/**\n * Publish contextual sidebar contributions from a PlatformReact page to the host sidebar, and\n * receive click dispatch by id. Accepts a discriminated `HostSidebarInput`:\n * - `mode:\"none\"` — suppress all plugin contributions without clobbering the static manifest.\n * - `mode:\"actions\"` — batch-2 Quick Actions for a detail entity (entity token + action list).\n * - `mode:\"detail\"` — full detail-nav replacement (title, nav items, back-arrow, Quick Actions).\n *\n * Serializable descriptors only cross to the host; the inline `onSelect` handlers stay in a\n * ref-map here and are always the latest closure. Fails closed (no publish/subscribe, no throw)\n * when the host predates or is incompatible with the seam. */\nexport function useHostSidebarActions(input: HostSidebarInput): void {\n const api = useContext(HostSidebarActionsContext);\n const pageId = useContext(SurfaceBaseContext)?.pageId; // diagnostic tag (per-surface)\n const compatible: HostSidebarActionsApi | null =\n api && isHostChromeCompatible(api.capabilities?.contractVersion) ? api : null;\n\n // Ref-map refreshed every render → dispatch always hits the latest handler (recursing groups).\n const actions = input.mode === SIDEBAR_MODE.none ? [] : input.actions;\n const handlers = useRef<Map<string, () => void>>(new Map());\n handlers.current = new Map(collectDispatchHandlers(actions));\n\n useEffect(() => {\n if (!compatible) {\n reportNoHost(pageId);\n return;\n }\n const unsub = compatible.subscribe((id) => {\n handlers.current.get(id)?.();\n });\n return () => {\n compatible.clear();\n unsub();\n };\n }, [compatible, pageId]);\n\n // Content key: mode + entityToken + descriptor tuples + (detail) nav/back/title.\n const key = contributionKey(input);\n useEffect(() => {\n if (!compatible) return;\n compatible.publish(toContribution(input));\n // key encodes all content that affects the published contribution; changes force republish.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [compatible, key]);\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { A11yPayload, BridgeClientContext, BridgePortShim, DensityPayload, ExtensionRuntimeProvider, ExtensionRuntimeProviderProps, HostIdentity, HostIdentityContext, HostIdentityUser, HostPermission, ItemsResponse, LocalePayload, McpTransport, NavPayload, PortBridgeClient, SessionTokenPayload, ThemePayload, UseMcpQueryOptions, UseMcpQueryResult, UseMcpResourceOptions, UseMcpResourceResult, UseMcpToolOptions, UseMcpToolResult, createPortBridgeClient, unwrapItems, useBridgeClient, useBridgeLocale, useBridgeTheme, useHostIdentity, useMcpQuery, useMcpResource, useMcpTool } from '@ethisyscore/extension-runtime/plugin';
2
2
  export { PlatformReactPageProps, definePlatformReactPage, emitNavigation } from '@ethisyscore/components-react';
3
- export { H as HOST_CHROME_CONTRACT_VERSION, a as HostChromeDiagnostic, b as HostSidebarAction, c as HostSidebarActionDescriptor, d as HostSidebarActionSlot, e as HostSidebarActionVariant, f as HostSidebarActionsApi, g as HostSidebarActionsCapabilities, h as HostSidebarActionsContext, i as HostSidebarActionsPageProps, j as HostSidebarContribution, k as HostSidebarInput, l as HostSidebarNavItem, S as SIDEBAR_MODE, m as isHostChromeCompatible, r as reportHostChromeDiagnostic, s as setHostChromeDiagnosticSink, u as useHostSidebarActions } from './useHostSidebarActions-DFZPen7T.cjs';
3
+ export { H as HOST_CHROME_CONTRACT_VERSION, a as HostChromeDiagnostic, b as HostSidebarAction, c as HostSidebarActionDescriptor, d as HostSidebarActionSlot, e as HostSidebarActionVariant, f as HostSidebarActionsApi, g as HostSidebarActionsCapabilities, h as HostSidebarActionsContext, i as HostSidebarActionsPageProps, j as HostSidebarContribution, k as HostSidebarInput, l as HostSidebarNavItem, S as SIDEBAR_MODE, m as isHostChromeCompatible, r as reportHostChromeDiagnostic, s as setHostChromeDiagnosticSink, u as useHostSidebarActions } from './useHostSidebarActions-CyNYWKiv.cjs';
4
4
  import 'react';
5
5
 
6
6
  /**
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { A11yPayload, BridgeClientContext, BridgePortShim, DensityPayload, ExtensionRuntimeProvider, ExtensionRuntimeProviderProps, HostIdentity, HostIdentityContext, HostIdentityUser, HostPermission, ItemsResponse, LocalePayload, McpTransport, NavPayload, PortBridgeClient, SessionTokenPayload, ThemePayload, UseMcpQueryOptions, UseMcpQueryResult, UseMcpResourceOptions, UseMcpResourceResult, UseMcpToolOptions, UseMcpToolResult, createPortBridgeClient, unwrapItems, useBridgeClient, useBridgeLocale, useBridgeTheme, useHostIdentity, useMcpQuery, useMcpResource, useMcpTool } from '@ethisyscore/extension-runtime/plugin';
2
2
  export { PlatformReactPageProps, definePlatformReactPage, emitNavigation } from '@ethisyscore/components-react';
3
- export { H as HOST_CHROME_CONTRACT_VERSION, a as HostChromeDiagnostic, b as HostSidebarAction, c as HostSidebarActionDescriptor, d as HostSidebarActionSlot, e as HostSidebarActionVariant, f as HostSidebarActionsApi, g as HostSidebarActionsCapabilities, h as HostSidebarActionsContext, i as HostSidebarActionsPageProps, j as HostSidebarContribution, k as HostSidebarInput, l as HostSidebarNavItem, S as SIDEBAR_MODE, m as isHostChromeCompatible, r as reportHostChromeDiagnostic, s as setHostChromeDiagnosticSink, u as useHostSidebarActions } from './useHostSidebarActions-DFZPen7T.js';
3
+ export { H as HOST_CHROME_CONTRACT_VERSION, a as HostChromeDiagnostic, b as HostSidebarAction, c as HostSidebarActionDescriptor, d as HostSidebarActionSlot, e as HostSidebarActionVariant, f as HostSidebarActionsApi, g as HostSidebarActionsCapabilities, h as HostSidebarActionsContext, i as HostSidebarActionsPageProps, j as HostSidebarContribution, k as HostSidebarInput, l as HostSidebarNavItem, S as SIDEBAR_MODE, m as isHostChromeCompatible, r as reportHostChromeDiagnostic, s as setHostChromeDiagnosticSink, u as useHostSidebarActions } from './useHostSidebarActions-CyNYWKiv.js';
4
4
  import 'react';
5
5
 
6
6
  /**
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ function useCurrentUser() {
19
19
  }
20
20
  var CONTEXT_KEY = /* @__PURE__ */ Symbol.for("ethisyscore.plugin-ui.hostSidebarActionsContext");
21
21
  var SINK_KEY = /* @__PURE__ */ Symbol.for("ethisyscore.plugin-ui.hostChromeDiagnosticSink");
22
- var HOST_CHROME_CONTRACT_VERSION = "2.0.0";
22
+ var HOST_CHROME_CONTRACT_VERSION = "2.1.0";
23
23
  var _g = globalThis;
24
24
  var HostSidebarActionsContext = _g[CONTEXT_KEY] ?? (_g[CONTEXT_KEY] = createContext(null));
25
25
  var _gs = globalThis;
@@ -52,8 +52,17 @@ function toDescriptor(a) {
52
52
  const { onSelect: _drop, ...rest } = a;
53
53
  return rest;
54
54
  }
55
+ if (a.kind === "group") {
56
+ const { children, ...rest } = a;
57
+ return { ...rest, children: children.map(toDescriptor) };
58
+ }
55
59
  return a;
56
60
  }
61
+ function collectDispatchHandlers(actions) {
62
+ return actions.flatMap(
63
+ (a) => a.kind === "dispatch" && a.onSelect ? [[a.id, a.onSelect]] : a.kind === "group" ? collectDispatchHandlers(a.children) : []
64
+ );
65
+ }
57
66
  function toContribution(input) {
58
67
  if (input.mode === SIDEBAR_MODE.none) {
59
68
  return { mode: SIDEBAR_MODE.none };
@@ -80,7 +89,7 @@ function contributionKey(input) {
80
89
  if (input.mode === SIDEBAR_MODE.none) {
81
90
  return JSON.stringify([SIDEBAR_MODE.none]);
82
91
  }
83
- const actionTuples = input.actions.map((a) => [
92
+ const actionTuple = (a) => [
84
93
  a.id,
85
94
  a.label,
86
95
  a.icon ?? "",
@@ -90,8 +99,11 @@ function contributionKey(input) {
90
99
  a.active ? 1 : 0,
91
100
  a.requiredPermission ?? "",
92
101
  a.kind,
93
- a.kind === "navigate" ? a.href : ""
94
- ]);
102
+ a.kind === "navigate" ? a.href : "",
103
+ // Group children fold into the key so a change to any collapsed leaf forces a republish.
104
+ a.kind === "group" ? a.children.map(actionTuple) : ""
105
+ ];
106
+ const actionTuples = input.actions.map(actionTuple);
95
107
  if (input.mode === SIDEBAR_MODE.actions) {
96
108
  return JSON.stringify([SIDEBAR_MODE.actions, input.entityToken, actionTuples]);
97
109
  }
@@ -124,11 +136,7 @@ function useHostSidebarActions(input) {
124
136
  const compatible = api && isHostChromeCompatible(api.capabilities?.contractVersion) ? api : null;
125
137
  const actions = input.mode === SIDEBAR_MODE.none ? [] : input.actions;
126
138
  const handlers = useRef(/* @__PURE__ */ new Map());
127
- handlers.current = new Map(
128
- actions.flatMap(
129
- (a) => a.kind === "dispatch" && a.onSelect ? [[a.id, a.onSelect]] : []
130
- )
131
- );
139
+ handlers.current = new Map(collectDispatchHandlers(actions));
132
140
  useEffect(() => {
133
141
  if (!compatible) {
134
142
  reportNoHost(pageId);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/currentUser.ts","../src/platform-react/hostSidebarActionsContext.ts","../src/platform-react/surfaceUrl.ts","../src/platform-react/useHostSidebarActions.ts"],"names":["createContext","useContext"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `useCurrentUser` — the display-only current-user hook for plugin frontends\n * (WI 5154 follow-up #2, plugin-ui host-identity).\n *\n * A plugin page often needs the signed-in user for UX bits — a \"you are signed\n * in as …\" hint, pre-selecting the caller in a sign-off panel, or a\n * \"my acknowledgements\" heading. This hook surfaces that identity from the\n * host without the plugin importing the lower-level {@link useHostIdentity}\n * seam directly.\n *\n * It is a thin adapter over the host-provided {@link HostIdentity} context\n * (bound by the host through `ExtensionRuntimeProvider`'s `identity` prop):\n * it projects the host's richer {@link HostIdentityUser} down to the minimal\n * `{ id, displayName }` shape a plugin page needs for display.\n *\n * ⚠️ SECURITY — DISPLAY ONLY. This is presentation/UX data, NOT an\n * authorization source. Authorization is enforced HOST-SIDE at the MCP\n * boundary: the plugin backend re-authorises every tool/resource call from the\n * trusted server session. Never gate a mutation or a data read on this value.\n *\n * Returns `null` when the host has not provided an identity — standalone / mock\n * hosts, a host that predates the identity seam, or while host auth is still\n * loading or the caller is unauthenticated. Callers MUST handle `null`\n * (this matches the pre-existing plugin `useAuth().user` shim, which returned\n * `undefined`, so adopting this hook is a no-regression change).\n */\nimport { useHostIdentity } from \"@ethisyscore/extension-runtime/plugin\";\n\n/**\n * Minimal current-user shape a plugin page reads for display. Mirrors the\n * identity the host authenticates: a stable `id` and a human-readable\n * `displayName`. (No email — the host identity seam does not currently\n * forward one; if that changes, extend {@link HostIdentityUser} first and\n * project it here.)\n */\nexport interface CurrentUser {\n /** Stable user id (matches the host's authenticated caller id). */\n id: string;\n /** Human-readable display name (the host's full name). */\n displayName: string;\n}\n\n/**\n * Returns the host-authenticated current user projected to the display-only\n * {@link CurrentUser} shape, or `null` when no host identity is available.\n *\n * Display-only — see the module doc: authorization stays host-enforced at the\n * MCP boundary. Do NOT use the returned value as an authorization decision.\n */\nexport function useCurrentUser(): CurrentUser | null {\n const identity = useHostIdentity();\n const user = identity?.user ?? null;\n if (!user) {\n return null;\n }\n return {\n id: user.id,\n // The host binds `fullName`; fall back to composing first + last so a host\n // that only sets the parts still yields a usable display name.\n displayName:\n user.fullName?.trim() || `${user.firstName ?? \"\"} ${user.lastName ?? \"\"}`.trim(),\n };\n}\n","import { createContext } from \"react\";\nimport type { Context } from \"react\";\nimport type { HostSidebarActionsApiShape } from \"@ethisyscore/components-react\";\n\n// ── Cross-bundle singleton keys ───────────────────────────────────────────────\n// tsup may inline this module into BOTH the package-root entry and the subpath\n// entry, producing two distinct module copies. Storing shared state on globalThis\n// under Symbol.for(…) keys guarantees that every copy resolves the SAME object.\nconst CONTEXT_KEY = Symbol.for(\"ethisyscore.plugin-ui.hostSidebarActionsContext\");\nconst SINK_KEY = Symbol.for(\"ethisyscore.plugin-ui.hostChromeDiagnosticSink\");\n\n/** Host-chrome / sidebar-actions contract version (semver). Host MAJOR must equal the SDK\n * MAJOR before any publish/subscribe; otherwise the plugin hook fails closed (WI 5188 batch 2). */\nexport const HOST_CHROME_CONTRACT_VERSION = \"2.0.0\";\n\nexport type HostSidebarActionSlot = \"primary\" | \"overflow\" | \"sidebar-only\";\nexport type HostSidebarActionVariant = \"default\" | \"danger\";\n\n/** Fields common to every action. */\ninterface HostSidebarActionBase {\n id: string;\n label: string;\n icon?: string;\n slot?: HostSidebarActionSlot;\n variant?: HostSidebarActionVariant;\n disabled?: boolean;\n active?: boolean;\n /** Optional host-side double-gate: the mask is resolved against the surface extension groupCode. */\n requiredPermission?: number;\n}\n\n/** Strictly serializable action the plugin publishes to the host sidebar — NO functions cross\n * the boundary. Discriminated union so `href` is REQUIRED at the type level for a navigate\n * action and FORBIDDEN for a dispatch action (spec-gate MEDIUM R1-M1 — a navigate with a\n * missing href fails typecheck at authoring time rather than being silently dropped by the host).\n * `kind:\"dispatch\"` is clicked back to the plugin by `id` via the action-emit subscription. */\nexport type HostSidebarActionDescriptor =\n | (HostSidebarActionBase & { kind: \"dispatch\" })\n | (HostSidebarActionBase & { kind: \"navigate\"; href: string }); // extension-relative; host normalizes/validates\n\n/** A navigation leaf the plugin drives into the host detail sidebar. `href` is extension-relative;\n * the host canonicalises + confines it to `/extensions/<slug>/…` (host-owned, never trusted). */\nexport interface HostSidebarNavItem {\n id: string;\n label: string;\n icon?: string;\n href: string;\n}\n\n/** The single publish payload. `none` = contribute nothing (do NOT clobber the static manifest\n * actions); `actions` = batch-2 Quick Actions for a detail entity; `detail` = full detail-nav\n * REPLACEMENT (title + nav items + back-arrow + Quick Actions). `identityEpoch` is host-stamped\n * (plan D-1), so it is not part of this plugin-published shape. */\nexport type HostSidebarContribution =\n | { mode: \"none\" }\n | { mode: \"actions\"; entityToken: string; actions: HostSidebarActionDescriptor[] }\n | {\n mode: \"detail\";\n entityToken: string;\n title: string;\n navSectionLabel: string;\n navItems: HostSidebarNavItem[];\n backHref: string;\n backLabel: string;\n actions: HostSidebarActionDescriptor[];\n };\n\nexport interface HostSidebarActionsCapabilities {\n readonly contractVersion: string; // HOST_CHROME_CONTRACT_VERSION the host implements\n}\n\n/** Host-provided API, passed to a PlatformReact page as the `hostSidebar` prop and read via\n * the plugin-bundled context. Additive-only. */\nexport interface HostSidebarActionsApi {\n /** Replace the surface's sidebar contribution. The host rejects a publish whose entityToken ≠\n * its current location-derived token, rejects duplicate action ids, and — for mode:\"detail\" —\n * rejects/atomically-drops an invalid or href-unsafe context (falls back to main nav). */\n publish(contribution: HostSidebarContribution): void;\n /** Revert to the static-manifest fallback (publication state = unpublished). */\n clear(): void;\n /** Subscribe to click dispatch for `kind:\"dispatch\"` actions. The host emits the clicked\n * action id. Returns an unsubscribe. The plugin bridge subscribes exactly once. */\n subscribe(onAction: (actionId: string) => void): () => void;\n readonly capabilities: HostSidebarActionsCapabilities;\n}\n\n/** Props the host injects into a page for the sidebar-actions seam (merged into PlatformReactPageProps).\n * Typed against the dependency-neutral `HostSidebarActionsApiShape` — the SAME type\n * `PlatformReactPageProps.hostSidebar` uses in `@ethisyscore/components-react` — so this\n * convenience alias never diverges from the canonical page-props declaration. The concrete\n * `HostSidebarActionsApi` is structurally assignable to the shape; the page bridge narrows to\n * it internally. */\nexport interface HostSidebarActionsPageProps {\n hostSidebar?: HostSidebarActionsApiShape;\n}\n\ntype GlobalWithCtx = typeof globalThis & {\n [CONTEXT_KEY]?: Context<HostSidebarActionsApi | null>;\n};\nconst _g = globalThis as GlobalWithCtx;\nexport const HostSidebarActionsContext: Context<HostSidebarActionsApi | null> =\n _g[CONTEXT_KEY] ?? (_g[CONTEXT_KEY] = createContext<HostSidebarActionsApi | null>(null));\n\n// ── Diagnostics adapter (concrete, testable — spec-gate HIGH R1-H3) ─────────────\n// A single injectable sink so degradation is OBSERVABLE in all environments without inventing a\n// `globalThis` global. The host runtime calls `setHostChromeDiagnosticSink` once to route these to\n// its real telemetry; absent a sink it is a safe no-op. Every emission carries the page id tag.\n//\n// The sink is stored on the SINK_KEY globalThis slot so that setHostChromeDiagnosticSink and\n// reportHostChromeDiagnostic in any duplicate module copy read/write the SAME slot.\nexport interface HostChromeDiagnostic {\n event: string;\n pageId?: string;\n detail?: unknown;\n}\ntype DiagnosticSinkFn = (d: HostChromeDiagnostic) => void;\ntype GlobalWithSink = typeof globalThis & { [SINK_KEY]?: DiagnosticSinkFn };\nconst _gs = globalThis as GlobalWithSink;\nif (_gs[SINK_KEY] === undefined) {\n _gs[SINK_KEY] = () => {};\n}\nexport function setHostChromeDiagnosticSink(sink: DiagnosticSinkFn): void {\n _gs[SINK_KEY] = sink;\n}\nexport function reportHostChromeDiagnostic(d: HostChromeDiagnostic): void {\n try {\n _gs[SINK_KEY]!(d);\n } catch {\n /* diagnostics are best-effort — never throw into the plugin */\n }\n}\n\n/** Major-equality compatibility check; fail-closed on absent/unparseable input. */\nexport function isHostChromeCompatible(hostVersion: string | undefined): boolean {\n if (hostVersion === undefined) return false;\n const hostMajor = Number.parseInt(hostVersion.split(\".\")[0] ?? \"\", 10);\n const sdkMajor = Number.parseInt(HOST_CHROME_CONTRACT_VERSION.split(\".\")[0] ?? \"\", 10);\n if (Number.isNaN(hostMajor) || Number.isNaN(sdkMajor)) return false;\n return hostMajor === sdkMajor;\n}\n","import { createContext, useContext } from \"react\";\nimport { useHostIdentity } from \"@ethisyscore/extension-runtime/plugin\";\n\n/** The surface's mount context, provided by definePlatformReactPluginPage. */\nexport interface SurfaceBaseValue {\n /** Host-provided mount path `/extensions/<slug>/<pageId>` (authoritative). */\n basePath?: string;\n /** The page id (from PlatformReactPageProps), used for the derived fallback. */\n pageId?: string;\n}\n\nexport const SurfaceBaseContext = createContext<SurfaceBaseValue | undefined>(undefined);\n\n/** Match the host's slug normalisation (useExtensionSurfaceShellPage `normaliseSlug`). */\nexport function normaliseSlug(value: string): string {\n return value.trim().toLowerCase();\n}\n\nfunction stripTrailingSlash(p: string): string {\n return p.length > 1 && p.endsWith(\"/\") ? p.slice(0, -1) : p;\n}\n\n/**\n * Resolve the surface base. Precedence: host-provided basePath → derived from\n * normaliseSlug(extensionGroupCode) + pageId → null (caller decides how to fail).\n * Pure — callers read context + identity and pass the pieces in.\n */\nexport function resolveSurfaceBase(input: {\n basePath?: string;\n pageId?: string;\n extensionGroupCode?: string | null;\n}): { base: string; groupRoot: string } | null {\n if (input.basePath) {\n const base = stripTrailingSlash(input.basePath);\n const cut = base.lastIndexOf(\"/\");\n const groupRoot = cut > 0 ? base.slice(0, cut) : base;\n return { base, groupRoot };\n }\n const slug = input.extensionGroupCode ? normaliseSlug(input.extensionGroupCode) : \"\";\n const pageId = input.pageId ?? \"\";\n if (slug && pageId) {\n const groupRoot = `/extensions/${slug}`;\n return { base: `${groupRoot}/${pageId}`, groupRoot };\n }\n return null;\n}\n\n/** Join base + sub (leading slash stripped) + optional ?query/#hash suffix. */\nexport function buildSurfaceUrl(base: string, sub = \"\", suffix = \"\"): string {\n const b = stripTrailingSlash(base);\n const s = sub.replace(/^\\/+/, \"\");\n const path = s ? `${b}/${s}` : b;\n return suffix ? `${path}${suffix}` : path;\n}\n\n/**\n * Hook: build a URL relative to the CURRENT surface. Precedence per\n * resolveSurfaceBase; throws in dev when unresolved (never returns app-root).\n */\nexport function useSurfaceUrl(): (sub?: string, suffix?: string) => string {\n const ctx = useContext(SurfaceBaseContext);\n const identity = useHostIdentity();\n return (sub = \"\", suffix = \"\") => {\n const resolved = resolveSurfaceBase({\n basePath: ctx?.basePath,\n pageId: ctx?.pageId,\n extensionGroupCode: identity?.extensionGroupCode ?? null,\n });\n if (!resolved) {\n const msg =\n \"useSurfaceUrl: no surface base — SurfaceBaseContext (basePath/pageId) and \" +\n \"extensionGroupCode are both unavailable. Ensure the page is wrapped by \" +\n \"definePlatformReactPluginPage inside the host runtime.\";\n // Dev: fail loud. Prod: never navigate to app-root (that recreates the 404\n // class) — stay on the current path (a no-op) so a transient unresolved\n // state (e.g. identity still loading on a pre-basePath host) can't crash the\n // surface. Callers invoke this in event handlers, by which time identity has\n // loaded and the base resolves normally.\n // Guard `process` access: in a browser/ESM consumer where `process` is\n // undefined, reading `process.env.NODE_ENV` directly would throw a\n // ReferenceError before the prod fallback runs. `typeof` never throws on an\n // undeclared identifier. Fail SAFE — throw ONLY when we can positively\n // confirm a non-production env; an unknown env (no `process`) is treated as\n // production so a bundled surface never crashes here.\n const isDevEnv =\n typeof process !== \"undefined\" && process.env?.NODE_ENV !== \"production\";\n if (isDevEnv) throw new Error(msg);\n if (typeof console !== \"undefined\") console.error(msg);\n // Prod no-op: stay on the FULL current URL (path + query + hash) — a true\n // no-op, and never app-root.\n return typeof window !== \"undefined\"\n ? window.location.pathname + window.location.search + window.location.hash\n : \"\";\n }\n return buildSurfaceUrl(resolved.base, sub, suffix);\n };\n}\n\n/** Pure: build a URL for ANOTHER surface (cross-surface, e.g. an overlay opening a page). */\nexport function surfacePathFor(opts: {\n slug: string;\n pageId: string;\n sub?: string;\n suffix?: string;\n}): string {\n const base = `/extensions/${normaliseSlug(opts.slug)}/${opts.pageId}`;\n return buildSurfaceUrl(base, opts.sub ?? \"\", opts.suffix ?? \"\");\n}\n","import { useContext, useEffect, useRef } from \"react\";\nimport {\n HostSidebarActionsContext,\n isHostChromeCompatible,\n reportHostChromeDiagnostic,\n type HostSidebarActionDescriptor,\n type HostSidebarActionsApi,\n type HostSidebarContribution,\n type HostSidebarNavItem,\n} from \"./hostSidebarActionsContext\";\nimport { SurfaceBaseContext } from \"./surfaceUrl\";\n\n/** Authoring shape: a descriptor plus — ONLY for `kind:\"dispatch\"` — an inline handler.\n * A distributive union (not a blanket intersection over the whole descriptor union): `onSelect`\n * is permitted only on a dispatch action. A navigate action carries no handler (the host performs\n * the navigation from `href`), so supplying `onSelect` on a navigate action is a typecheck error\n * rather than a silently-ignored footgun. The hook strips `onSelect` before publishing (only\n * serializable descriptors cross to the host). */\nexport type HostSidebarAction =\n | (Extract<HostSidebarActionDescriptor, { kind: \"dispatch\" }> & { onSelect?: () => void })\n | Extract<HostSidebarActionDescriptor, { kind: \"navigate\" }>;\n\n/** Single source for the discriminant values used across the mode checks, `toContribution`, and\n * the content key — so the \"none\"/\"actions\"/\"detail\" strings live in one place. The union type\n * below intentionally keeps its literal members (they ARE the discriminant + drive IntelliSense);\n * `SIDEBAR_MODE.*` is `as const`, so it is type-identical to those literals. */\nexport const SIDEBAR_MODE = { none: \"none\", actions: \"actions\", detail: \"detail\" } as const;\n\n/** Discriminated input to `useHostSidebarActions`. `mode:\"none\"` suppresses all plugin-side\n * contributions without clobbering the static manifest actions. `mode:\"actions\"` is the\n * batch-2 Quick Actions shape. `mode:\"detail\"` is the full detail-nav replacement with title,\n * navigation items, back-arrow, and Quick Actions. */\nexport type HostSidebarInput =\n | { mode: \"none\" }\n | { mode: \"actions\"; entityToken: string; actions: HostSidebarAction[] }\n | {\n mode: \"detail\";\n entityToken: string;\n title: string;\n navSectionLabel: string;\n navItems: HostSidebarNavItem[];\n backHref: string;\n backLabel: string;\n actions: HostSidebarAction[];\n };\n\nfunction toDescriptor(a: HostSidebarAction): HostSidebarActionDescriptor {\n if (a.kind === \"dispatch\") {\n const { onSelect: _drop, ...rest } = a;\n return rest; // rest is the serializable dispatch descriptor\n }\n return a; // navigate descriptor is already serializable (no handler)\n}\n\n/** Map a plugin-authoring input to the serializable contribution published to the host.\n * `onSelect` handlers are stripped via `toDescriptor`; nav/back/title pass through for `detail`. */\nfunction toContribution(input: HostSidebarInput): HostSidebarContribution {\n if (input.mode === SIDEBAR_MODE.none) {\n return { mode: SIDEBAR_MODE.none };\n }\n if (input.mode === SIDEBAR_MODE.actions) {\n return {\n mode: SIDEBAR_MODE.actions,\n entityToken: input.entityToken,\n actions: input.actions.map(toDescriptor),\n };\n }\n // mode === \"detail\"\n return {\n mode: SIDEBAR_MODE.detail,\n entityToken: input.entityToken,\n title: input.title,\n navSectionLabel: input.navSectionLabel,\n navItems: input.navItems,\n backHref: input.backHref,\n backLabel: input.backLabel,\n actions: input.actions.map(toDescriptor),\n };\n}\n\n/** Content key so we republish only when the visible descriptor set changes. JSON-encodes an\n * ARRAY of ORDERED-tuple arrays: element order is stable (unlike object property insertion order,\n * which is why we avoid `JSON.stringify` over the raw descriptor objects — spec-gate MEDIUM R1-M2),\n * and JSON string-escaping makes the key delimiter-collision-proof, so a `label`/`href` that\n * happens to contain a separator character can no longer alias two distinct action sets to the\n * same key (which would skip a needed republish and leave the host sidebar stale). */\nfunction contributionKey(input: HostSidebarInput): string {\n if (input.mode === SIDEBAR_MODE.none) {\n return JSON.stringify([SIDEBAR_MODE.none]);\n }\n const actionTuples = input.actions.map((a) => [\n a.id,\n a.label,\n a.icon ?? \"\",\n a.slot ?? \"\",\n a.variant ?? \"\",\n a.disabled ? 1 : 0,\n a.active ? 1 : 0,\n a.requiredPermission ?? \"\",\n a.kind,\n a.kind === \"navigate\" ? a.href : \"\",\n ]);\n if (input.mode === SIDEBAR_MODE.actions) {\n return JSON.stringify([SIDEBAR_MODE.actions, input.entityToken, actionTuples]);\n }\n // mode === \"detail\"\n const navTuples = input.navItems.map((n) => [n.id, n.label, n.icon ?? \"\", n.href]);\n return JSON.stringify([\n SIDEBAR_MODE.detail,\n input.entityToken,\n input.title,\n input.navSectionLabel,\n input.backHref,\n input.backLabel,\n navTuples,\n actionTuples,\n ]);\n}\n\n/** Per-page dev-warn throttle (NOT a global boolean — a module singleton would suppress warnings\n * for every later surface in a shared bundle; spec-gate HIGH R1-H2). Telemetry fires every time. */\nconst devWarnedPages = new Set<string>();\nfunction reportNoHost(pageId: string | undefined): void {\n // Observable in ALL environments via the injected diagnostic sink, tagged by page id (R1-H3).\n reportHostChromeDiagnostic({ event: \"host_sidebar_actions_unavailable\", pageId });\n const key = pageId ?? \"*\";\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((import.meta as any).env?.DEV && !devWarnedPages.has(key)) {\n devWarnedPages.add(key);\n // eslint-disable-next-line no-console\n console.warn(\n `[plugin-ui] useHostSidebarActions (${key}): no compatible host sidebar-actions API — ` +\n \"the host predates the seam or its contract major differs. Quick Actions will not render.\",\n );\n }\n}\n\n/**\n * Publish contextual sidebar contributions from a PlatformReact page to the host sidebar, and\n * receive click dispatch by id. Accepts a discriminated `HostSidebarInput`:\n * - `mode:\"none\"` — suppress all plugin contributions without clobbering the static manifest.\n * - `mode:\"actions\"` — batch-2 Quick Actions for a detail entity (entity token + action list).\n * - `mode:\"detail\"` — full detail-nav replacement (title, nav items, back-arrow, Quick Actions).\n *\n * Serializable descriptors only cross to the host; the inline `onSelect` handlers stay in a\n * ref-map here and are always the latest closure. Fails closed (no publish/subscribe, no throw)\n * when the host predates or is incompatible with the seam. */\nexport function useHostSidebarActions(input: HostSidebarInput): void {\n const api = useContext(HostSidebarActionsContext);\n const pageId = useContext(SurfaceBaseContext)?.pageId; // diagnostic tag (per-surface)\n const compatible: HostSidebarActionsApi | null =\n api && isHostChromeCompatible(api.capabilities?.contractVersion) ? api : null;\n\n // Ref-map refreshed every render → dispatch always hits the latest handler.\n const actions = input.mode === SIDEBAR_MODE.none ? [] : input.actions;\n const handlers = useRef<Map<string, () => void>>(new Map());\n handlers.current = new Map(\n actions.flatMap((a) =>\n a.kind === \"dispatch\" && a.onSelect ? [[a.id, a.onSelect] as const] : [],\n ),\n );\n\n useEffect(() => {\n if (!compatible) {\n reportNoHost(pageId);\n return;\n }\n const unsub = compatible.subscribe((id) => {\n handlers.current.get(id)?.();\n });\n return () => {\n compatible.clear();\n unsub();\n };\n }, [compatible, pageId]);\n\n // Content key: mode + entityToken + descriptor tuples + (detail) nav/back/title.\n const key = contributionKey(input);\n useEffect(() => {\n if (!compatible) return;\n compatible.publish(toContribution(input));\n // key encodes all content that affects the published contribution; changes force republish.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [compatible, key]);\n}\n"]}
1
+ {"version":3,"sources":["../src/currentUser.ts","../src/platform-react/hostSidebarActionsContext.ts","../src/platform-react/surfaceUrl.ts","../src/platform-react/useHostSidebarActions.ts"],"names":["createContext","useContext"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `useCurrentUser` — the display-only current-user hook for plugin frontends\n * (WI 5154 follow-up #2, plugin-ui host-identity).\n *\n * A plugin page often needs the signed-in user for UX bits — a \"you are signed\n * in as …\" hint, pre-selecting the caller in a sign-off panel, or a\n * \"my acknowledgements\" heading. This hook surfaces that identity from the\n * host without the plugin importing the lower-level {@link useHostIdentity}\n * seam directly.\n *\n * It is a thin adapter over the host-provided {@link HostIdentity} context\n * (bound by the host through `ExtensionRuntimeProvider`'s `identity` prop):\n * it projects the host's richer {@link HostIdentityUser} down to the minimal\n * `{ id, displayName }` shape a plugin page needs for display.\n *\n * ⚠️ SECURITY — DISPLAY ONLY. This is presentation/UX data, NOT an\n * authorization source. Authorization is enforced HOST-SIDE at the MCP\n * boundary: the plugin backend re-authorises every tool/resource call from the\n * trusted server session. Never gate a mutation or a data read on this value.\n *\n * Returns `null` when the host has not provided an identity — standalone / mock\n * hosts, a host that predates the identity seam, or while host auth is still\n * loading or the caller is unauthenticated. Callers MUST handle `null`\n * (this matches the pre-existing plugin `useAuth().user` shim, which returned\n * `undefined`, so adopting this hook is a no-regression change).\n */\nimport { useHostIdentity } from \"@ethisyscore/extension-runtime/plugin\";\n\n/**\n * Minimal current-user shape a plugin page reads for display. Mirrors the\n * identity the host authenticates: a stable `id` and a human-readable\n * `displayName`. (No email — the host identity seam does not currently\n * forward one; if that changes, extend {@link HostIdentityUser} first and\n * project it here.)\n */\nexport interface CurrentUser {\n /** Stable user id (matches the host's authenticated caller id). */\n id: string;\n /** Human-readable display name (the host's full name). */\n displayName: string;\n}\n\n/**\n * Returns the host-authenticated current user projected to the display-only\n * {@link CurrentUser} shape, or `null` when no host identity is available.\n *\n * Display-only — see the module doc: authorization stays host-enforced at the\n * MCP boundary. Do NOT use the returned value as an authorization decision.\n */\nexport function useCurrentUser(): CurrentUser | null {\n const identity = useHostIdentity();\n const user = identity?.user ?? null;\n if (!user) {\n return null;\n }\n return {\n id: user.id,\n // The host binds `fullName`; fall back to composing first + last so a host\n // that only sets the parts still yields a usable display name.\n displayName:\n user.fullName?.trim() || `${user.firstName ?? \"\"} ${user.lastName ?? \"\"}`.trim(),\n };\n}\n","import { createContext } from \"react\";\nimport type { Context } from \"react\";\nimport type { HostSidebarActionsApiShape } from \"@ethisyscore/components-react\";\n\n// ── Cross-bundle singleton keys ───────────────────────────────────────────────\n// tsup may inline this module into BOTH the package-root entry and the subpath\n// entry, producing two distinct module copies. Storing shared state on globalThis\n// under Symbol.for(…) keys guarantees that every copy resolves the SAME object.\nconst CONTEXT_KEY = Symbol.for(\"ethisyscore.plugin-ui.hostSidebarActionsContext\");\nconst SINK_KEY = Symbol.for(\"ethisyscore.plugin-ui.hostChromeDiagnosticSink\");\n\n/** Host-chrome / sidebar-actions contract version (semver). Host MAJOR must equal the SDK\n * MAJOR before any publish/subscribe; otherwise the plugin hook fails closed (WI 5188 batch 2).\n * 2.1.0 added the additive `kind:\"group\"` action variant (collapsible Lifecycle/Share); the\n * major-equality gate keeps 2.0 and 2.1 mutually compatible (older hosts ignore group children). */\nexport const HOST_CHROME_CONTRACT_VERSION = \"2.1.0\";\n\nexport type HostSidebarActionSlot = \"primary\" | \"overflow\" | \"sidebar-only\";\nexport type HostSidebarActionVariant = \"default\" | \"danger\";\n\n/** Fields common to every action. */\ninterface HostSidebarActionBase {\n id: string;\n label: string;\n icon?: string;\n slot?: HostSidebarActionSlot;\n variant?: HostSidebarActionVariant;\n disabled?: boolean;\n active?: boolean;\n /** Optional host-side double-gate: the mask is resolved against the surface extension groupCode. */\n requiredPermission?: number;\n}\n\n/** Strictly serializable action the plugin publishes to the host sidebar — NO functions cross\n * the boundary. Discriminated union so `href` is REQUIRED at the type level for a navigate\n * action and FORBIDDEN for a dispatch action (spec-gate MEDIUM R1-M1 — a navigate with a\n * missing href fails typecheck at authoring time rather than being silently dropped by the host).\n * `kind:\"dispatch\"` is clicked back to the plugin by `id` via the action-emit subscription. */\nexport type HostSidebarActionDescriptor =\n | (HostSidebarActionBase & { kind: \"dispatch\" })\n | (HostSidebarActionBase & { kind: \"navigate\"; href: string }) // extension-relative; host normalizes/validates\n // A collapsible parent (e.g. Lifecycle / Share). It carries no behaviour of its own — the host\n // renders it as an accordion and its `children` as the collapsed rows. `children` is recursive:\n // each child is a navigate/dispatch action OR a nested group.\n | (HostSidebarActionBase & { kind: \"group\"; children: HostSidebarActionDescriptor[] });\n\n/** A navigation leaf the plugin drives into the host detail sidebar. `href` is extension-relative;\n * the host canonicalises + confines it to `/extensions/<slug>/…` (host-owned, never trusted). */\nexport interface HostSidebarNavItem {\n id: string;\n label: string;\n icon?: string;\n href: string;\n}\n\n/** The single publish payload. `none` = contribute nothing (do NOT clobber the static manifest\n * actions); `actions` = batch-2 Quick Actions for a detail entity; `detail` = full detail-nav\n * REPLACEMENT (title + nav items + back-arrow + Quick Actions). `identityEpoch` is host-stamped\n * (plan D-1), so it is not part of this plugin-published shape. */\nexport type HostSidebarContribution =\n | { mode: \"none\" }\n | { mode: \"actions\"; entityToken: string; actions: HostSidebarActionDescriptor[] }\n | {\n mode: \"detail\";\n entityToken: string;\n title: string;\n navSectionLabel: string;\n navItems: HostSidebarNavItem[];\n backHref: string;\n backLabel: string;\n actions: HostSidebarActionDescriptor[];\n };\n\nexport interface HostSidebarActionsCapabilities {\n readonly contractVersion: string; // HOST_CHROME_CONTRACT_VERSION the host implements\n}\n\n/** Host-provided API, passed to a PlatformReact page as the `hostSidebar` prop and read via\n * the plugin-bundled context. Additive-only. */\nexport interface HostSidebarActionsApi {\n /** Replace the surface's sidebar contribution. The host rejects a publish whose entityToken ≠\n * its current location-derived token, rejects duplicate action ids, and — for mode:\"detail\" —\n * rejects/atomically-drops an invalid or href-unsafe context (falls back to main nav). */\n publish(contribution: HostSidebarContribution): void;\n /** Revert to the static-manifest fallback (publication state = unpublished). */\n clear(): void;\n /** Subscribe to click dispatch for `kind:\"dispatch\"` actions. The host emits the clicked\n * action id. Returns an unsubscribe. The plugin bridge subscribes exactly once. */\n subscribe(onAction: (actionId: string) => void): () => void;\n readonly capabilities: HostSidebarActionsCapabilities;\n}\n\n/** Props the host injects into a page for the sidebar-actions seam (merged into PlatformReactPageProps).\n * Typed against the dependency-neutral `HostSidebarActionsApiShape` — the SAME type\n * `PlatformReactPageProps.hostSidebar` uses in `@ethisyscore/components-react` — so this\n * convenience alias never diverges from the canonical page-props declaration. The concrete\n * `HostSidebarActionsApi` is structurally assignable to the shape; the page bridge narrows to\n * it internally. */\nexport interface HostSidebarActionsPageProps {\n hostSidebar?: HostSidebarActionsApiShape;\n}\n\ntype GlobalWithCtx = typeof globalThis & {\n [CONTEXT_KEY]?: Context<HostSidebarActionsApi | null>;\n};\nconst _g = globalThis as GlobalWithCtx;\nexport const HostSidebarActionsContext: Context<HostSidebarActionsApi | null> =\n _g[CONTEXT_KEY] ?? (_g[CONTEXT_KEY] = createContext<HostSidebarActionsApi | null>(null));\n\n// ── Diagnostics adapter (concrete, testable — spec-gate HIGH R1-H3) ─────────────\n// A single injectable sink so degradation is OBSERVABLE in all environments without inventing a\n// `globalThis` global. The host runtime calls `setHostChromeDiagnosticSink` once to route these to\n// its real telemetry; absent a sink it is a safe no-op. Every emission carries the page id tag.\n//\n// The sink is stored on the SINK_KEY globalThis slot so that setHostChromeDiagnosticSink and\n// reportHostChromeDiagnostic in any duplicate module copy read/write the SAME slot.\nexport interface HostChromeDiagnostic {\n event: string;\n pageId?: string;\n detail?: unknown;\n}\ntype DiagnosticSinkFn = (d: HostChromeDiagnostic) => void;\ntype GlobalWithSink = typeof globalThis & { [SINK_KEY]?: DiagnosticSinkFn };\nconst _gs = globalThis as GlobalWithSink;\nif (_gs[SINK_KEY] === undefined) {\n _gs[SINK_KEY] = () => {};\n}\nexport function setHostChromeDiagnosticSink(sink: DiagnosticSinkFn): void {\n _gs[SINK_KEY] = sink;\n}\nexport function reportHostChromeDiagnostic(d: HostChromeDiagnostic): void {\n try {\n _gs[SINK_KEY]!(d);\n } catch {\n /* diagnostics are best-effort — never throw into the plugin */\n }\n}\n\n/** Major-equality compatibility check; fail-closed on absent/unparseable input. */\nexport function isHostChromeCompatible(hostVersion: string | undefined): boolean {\n if (hostVersion === undefined) return false;\n const hostMajor = Number.parseInt(hostVersion.split(\".\")[0] ?? \"\", 10);\n const sdkMajor = Number.parseInt(HOST_CHROME_CONTRACT_VERSION.split(\".\")[0] ?? \"\", 10);\n if (Number.isNaN(hostMajor) || Number.isNaN(sdkMajor)) return false;\n return hostMajor === sdkMajor;\n}\n","import { createContext, useContext } from \"react\";\nimport { useHostIdentity } from \"@ethisyscore/extension-runtime/plugin\";\n\n/** The surface's mount context, provided by definePlatformReactPluginPage. */\nexport interface SurfaceBaseValue {\n /** Host-provided mount path `/extensions/<slug>/<pageId>` (authoritative). */\n basePath?: string;\n /** The page id (from PlatformReactPageProps), used for the derived fallback. */\n pageId?: string;\n}\n\nexport const SurfaceBaseContext = createContext<SurfaceBaseValue | undefined>(undefined);\n\n/** Match the host's slug normalisation (useExtensionSurfaceShellPage `normaliseSlug`). */\nexport function normaliseSlug(value: string): string {\n return value.trim().toLowerCase();\n}\n\nfunction stripTrailingSlash(p: string): string {\n return p.length > 1 && p.endsWith(\"/\") ? p.slice(0, -1) : p;\n}\n\n/**\n * Resolve the surface base. Precedence: host-provided basePath → derived from\n * normaliseSlug(extensionGroupCode) + pageId → null (caller decides how to fail).\n * Pure — callers read context + identity and pass the pieces in.\n */\nexport function resolveSurfaceBase(input: {\n basePath?: string;\n pageId?: string;\n extensionGroupCode?: string | null;\n}): { base: string; groupRoot: string } | null {\n if (input.basePath) {\n const base = stripTrailingSlash(input.basePath);\n const cut = base.lastIndexOf(\"/\");\n const groupRoot = cut > 0 ? base.slice(0, cut) : base;\n return { base, groupRoot };\n }\n const slug = input.extensionGroupCode ? normaliseSlug(input.extensionGroupCode) : \"\";\n const pageId = input.pageId ?? \"\";\n if (slug && pageId) {\n const groupRoot = `/extensions/${slug}`;\n return { base: `${groupRoot}/${pageId}`, groupRoot };\n }\n return null;\n}\n\n/** Join base + sub (leading slash stripped) + optional ?query/#hash suffix. */\nexport function buildSurfaceUrl(base: string, sub = \"\", suffix = \"\"): string {\n const b = stripTrailingSlash(base);\n const s = sub.replace(/^\\/+/, \"\");\n const path = s ? `${b}/${s}` : b;\n return suffix ? `${path}${suffix}` : path;\n}\n\n/**\n * Hook: build a URL relative to the CURRENT surface. Precedence per\n * resolveSurfaceBase; throws in dev when unresolved (never returns app-root).\n */\nexport function useSurfaceUrl(): (sub?: string, suffix?: string) => string {\n const ctx = useContext(SurfaceBaseContext);\n const identity = useHostIdentity();\n return (sub = \"\", suffix = \"\") => {\n const resolved = resolveSurfaceBase({\n basePath: ctx?.basePath,\n pageId: ctx?.pageId,\n extensionGroupCode: identity?.extensionGroupCode ?? null,\n });\n if (!resolved) {\n const msg =\n \"useSurfaceUrl: no surface base — SurfaceBaseContext (basePath/pageId) and \" +\n \"extensionGroupCode are both unavailable. Ensure the page is wrapped by \" +\n \"definePlatformReactPluginPage inside the host runtime.\";\n // Dev: fail loud. Prod: never navigate to app-root (that recreates the 404\n // class) — stay on the current path (a no-op) so a transient unresolved\n // state (e.g. identity still loading on a pre-basePath host) can't crash the\n // surface. Callers invoke this in event handlers, by which time identity has\n // loaded and the base resolves normally.\n // Guard `process` access: in a browser/ESM consumer where `process` is\n // undefined, reading `process.env.NODE_ENV` directly would throw a\n // ReferenceError before the prod fallback runs. `typeof` never throws on an\n // undeclared identifier. Fail SAFE — throw ONLY when we can positively\n // confirm a non-production env; an unknown env (no `process`) is treated as\n // production so a bundled surface never crashes here.\n const isDevEnv =\n typeof process !== \"undefined\" && process.env?.NODE_ENV !== \"production\";\n if (isDevEnv) throw new Error(msg);\n if (typeof console !== \"undefined\") console.error(msg);\n // Prod no-op: stay on the FULL current URL (path + query + hash) — a true\n // no-op, and never app-root.\n return typeof window !== \"undefined\"\n ? window.location.pathname + window.location.search + window.location.hash\n : \"\";\n }\n return buildSurfaceUrl(resolved.base, sub, suffix);\n };\n}\n\n/** Pure: build a URL for ANOTHER surface (cross-surface, e.g. an overlay opening a page). */\nexport function surfacePathFor(opts: {\n slug: string;\n pageId: string;\n sub?: string;\n suffix?: string;\n}): string {\n const base = `/extensions/${normaliseSlug(opts.slug)}/${opts.pageId}`;\n return buildSurfaceUrl(base, opts.sub ?? \"\", opts.suffix ?? \"\");\n}\n","import { useContext, useEffect, useRef } from \"react\";\nimport {\n HostSidebarActionsContext,\n isHostChromeCompatible,\n reportHostChromeDiagnostic,\n type HostSidebarActionDescriptor,\n type HostSidebarActionsApi,\n type HostSidebarContribution,\n type HostSidebarNavItem,\n} from \"./hostSidebarActionsContext\";\nimport { SurfaceBaseContext } from \"./surfaceUrl\";\n\n/** Authoring shape: a descriptor plus — ONLY for `kind:\"dispatch\"` — an inline handler.\n * A distributive union (not a blanket intersection over the whole descriptor union): `onSelect`\n * is permitted only on a dispatch action. A navigate action carries no handler (the host performs\n * the navigation from `href`), so supplying `onSelect` on a navigate action is a typecheck error\n * rather than a silently-ignored footgun. The hook strips `onSelect` before publishing (only\n * serializable descriptors cross to the host). */\nexport type HostSidebarAction =\n | (Extract<HostSidebarActionDescriptor, { kind: \"dispatch\" }> & { onSelect?: () => void })\n | Extract<HostSidebarActionDescriptor, { kind: \"navigate\" }>\n // A collapsible group whose `children` are themselves authoring actions (so a dispatch child can\n // still carry its inline `onSelect`). `children` overrides the descriptor's serializable children.\n | (Omit<Extract<HostSidebarActionDescriptor, { kind: \"group\" }>, \"children\"> & { children: HostSidebarAction[] });\n\n/** Single source for the discriminant values used across the mode checks, `toContribution`, and\n * the content key — so the \"none\"/\"actions\"/\"detail\" strings live in one place. The union type\n * below intentionally keeps its literal members (they ARE the discriminant + drive IntelliSense);\n * `SIDEBAR_MODE.*` is `as const`, so it is type-identical to those literals. */\nexport const SIDEBAR_MODE = { none: \"none\", actions: \"actions\", detail: \"detail\" } as const;\n\n/** Discriminated input to `useHostSidebarActions`. `mode:\"none\"` suppresses all plugin-side\n * contributions without clobbering the static manifest actions. `mode:\"actions\"` is the\n * batch-2 Quick Actions shape. `mode:\"detail\"` is the full detail-nav replacement with title,\n * navigation items, back-arrow, and Quick Actions. */\nexport type HostSidebarInput =\n | { mode: \"none\" }\n | { mode: \"actions\"; entityToken: string; actions: HostSidebarAction[] }\n | {\n mode: \"detail\";\n entityToken: string;\n title: string;\n navSectionLabel: string;\n navItems: HostSidebarNavItem[];\n backHref: string;\n backLabel: string;\n actions: HostSidebarAction[];\n };\n\nfunction toDescriptor(a: HostSidebarAction): HostSidebarActionDescriptor {\n if (a.kind === \"dispatch\") {\n const { onSelect: _drop, ...rest } = a;\n return rest; // rest is the serializable dispatch descriptor\n }\n if (a.kind === \"group\") {\n // Recurse: strip the children's `onSelect` handlers so only serializable descriptors cross.\n const { children, ...rest } = a;\n return { ...rest, children: children.map(toDescriptor) };\n }\n return a; // navigate descriptor is already serializable (no handler)\n}\n\n/** Flatten a (possibly nested) action tree to its `kind:\"dispatch\"` handler entries — a group's\n * child dispatch clicks must reach their `onSelect` just like a top-level dispatch action. */\nfunction collectDispatchHandlers(actions: HostSidebarAction[]): Array<readonly [string, () => void]> {\n return actions.flatMap((a) =>\n a.kind === \"dispatch\" && a.onSelect\n ? [[a.id, a.onSelect] as const]\n : a.kind === \"group\"\n ? collectDispatchHandlers(a.children)\n : [],\n );\n}\n\n/** Map a plugin-authoring input to the serializable contribution published to the host.\n * `onSelect` handlers are stripped via `toDescriptor`; nav/back/title pass through for `detail`. */\nfunction toContribution(input: HostSidebarInput): HostSidebarContribution {\n if (input.mode === SIDEBAR_MODE.none) {\n return { mode: SIDEBAR_MODE.none };\n }\n if (input.mode === SIDEBAR_MODE.actions) {\n return {\n mode: SIDEBAR_MODE.actions,\n entityToken: input.entityToken,\n actions: input.actions.map(toDescriptor),\n };\n }\n // mode === \"detail\"\n return {\n mode: SIDEBAR_MODE.detail,\n entityToken: input.entityToken,\n title: input.title,\n navSectionLabel: input.navSectionLabel,\n navItems: input.navItems,\n backHref: input.backHref,\n backLabel: input.backLabel,\n actions: input.actions.map(toDescriptor),\n };\n}\n\n/** Content key so we republish only when the visible descriptor set changes. JSON-encodes an\n * ARRAY of ORDERED-tuple arrays: element order is stable (unlike object property insertion order,\n * which is why we avoid `JSON.stringify` over the raw descriptor objects — spec-gate MEDIUM R1-M2),\n * and JSON string-escaping makes the key delimiter-collision-proof, so a `label`/`href` that\n * happens to contain a separator character can no longer alias two distinct action sets to the\n * same key (which would skip a needed republish and leave the host sidebar stale). */\nfunction contributionKey(input: HostSidebarInput): string {\n if (input.mode === SIDEBAR_MODE.none) {\n return JSON.stringify([SIDEBAR_MODE.none]);\n }\n const actionTuple = (a: HostSidebarAction): unknown[] => [\n a.id,\n a.label,\n a.icon ?? \"\",\n a.slot ?? \"\",\n a.variant ?? \"\",\n a.disabled ? 1 : 0,\n a.active ? 1 : 0,\n a.requiredPermission ?? \"\",\n a.kind,\n a.kind === \"navigate\" ? a.href : \"\",\n // Group children fold into the key so a change to any collapsed leaf forces a republish.\n a.kind === \"group\" ? a.children.map(actionTuple) : \"\",\n ];\n const actionTuples = input.actions.map(actionTuple);\n if (input.mode === SIDEBAR_MODE.actions) {\n return JSON.stringify([SIDEBAR_MODE.actions, input.entityToken, actionTuples]);\n }\n // mode === \"detail\"\n const navTuples = input.navItems.map((n) => [n.id, n.label, n.icon ?? \"\", n.href]);\n return JSON.stringify([\n SIDEBAR_MODE.detail,\n input.entityToken,\n input.title,\n input.navSectionLabel,\n input.backHref,\n input.backLabel,\n navTuples,\n actionTuples,\n ]);\n}\n\n/** Per-page dev-warn throttle (NOT a global boolean — a module singleton would suppress warnings\n * for every later surface in a shared bundle; spec-gate HIGH R1-H2). Telemetry fires every time. */\nconst devWarnedPages = new Set<string>();\nfunction reportNoHost(pageId: string | undefined): void {\n // Observable in ALL environments via the injected diagnostic sink, tagged by page id (R1-H3).\n reportHostChromeDiagnostic({ event: \"host_sidebar_actions_unavailable\", pageId });\n const key = pageId ?? \"*\";\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((import.meta as any).env?.DEV && !devWarnedPages.has(key)) {\n devWarnedPages.add(key);\n // eslint-disable-next-line no-console\n console.warn(\n `[plugin-ui] useHostSidebarActions (${key}): no compatible host sidebar-actions API — ` +\n \"the host predates the seam or its contract major differs. Quick Actions will not render.\",\n );\n }\n}\n\n/**\n * Publish contextual sidebar contributions from a PlatformReact page to the host sidebar, and\n * receive click dispatch by id. Accepts a discriminated `HostSidebarInput`:\n * - `mode:\"none\"` — suppress all plugin contributions without clobbering the static manifest.\n * - `mode:\"actions\"` — batch-2 Quick Actions for a detail entity (entity token + action list).\n * - `mode:\"detail\"` — full detail-nav replacement (title, nav items, back-arrow, Quick Actions).\n *\n * Serializable descriptors only cross to the host; the inline `onSelect` handlers stay in a\n * ref-map here and are always the latest closure. Fails closed (no publish/subscribe, no throw)\n * when the host predates or is incompatible with the seam. */\nexport function useHostSidebarActions(input: HostSidebarInput): void {\n const api = useContext(HostSidebarActionsContext);\n const pageId = useContext(SurfaceBaseContext)?.pageId; // diagnostic tag (per-surface)\n const compatible: HostSidebarActionsApi | null =\n api && isHostChromeCompatible(api.capabilities?.contractVersion) ? api : null;\n\n // Ref-map refreshed every render → dispatch always hits the latest handler (recursing groups).\n const actions = input.mode === SIDEBAR_MODE.none ? [] : input.actions;\n const handlers = useRef<Map<string, () => void>>(new Map());\n handlers.current = new Map(collectDispatchHandlers(actions));\n\n useEffect(() => {\n if (!compatible) {\n reportNoHost(pageId);\n return;\n }\n const unsub = compatible.subscribe((id) => {\n handlers.current.get(id)?.();\n });\n return () => {\n compatible.clear();\n unsub();\n };\n }, [compatible, pageId]);\n\n // Content key: mode + entityToken + descriptor tuples + (detail) nav/back/title.\n const key = contributionKey(input);\n useEffect(() => {\n if (!compatible) return;\n compatible.publish(toContribution(input));\n // key encodes all content that affects the published contribution; changes force republish.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [compatible, key]);\n}\n"]}
@@ -74,7 +74,7 @@ function surfacePathFor(opts) {
74
74
  }
75
75
  var CONTEXT_KEY = /* @__PURE__ */ Symbol.for("ethisyscore.plugin-ui.hostSidebarActionsContext");
76
76
  var SINK_KEY = /* @__PURE__ */ Symbol.for("ethisyscore.plugin-ui.hostChromeDiagnosticSink");
77
- var HOST_CHROME_CONTRACT_VERSION = "2.0.0";
77
+ var HOST_CHROME_CONTRACT_VERSION = "2.1.0";
78
78
  var _g = globalThis;
79
79
  var HostSidebarActionsContext = _g[CONTEXT_KEY] ?? (_g[CONTEXT_KEY] = react.createContext(null));
80
80
  var _gs = globalThis;
@@ -551,8 +551,17 @@ function toDescriptor(a) {
551
551
  const { onSelect: _drop, ...rest } = a;
552
552
  return rest;
553
553
  }
554
+ if (a.kind === "group") {
555
+ const { children, ...rest } = a;
556
+ return { ...rest, children: children.map(toDescriptor) };
557
+ }
554
558
  return a;
555
559
  }
560
+ function collectDispatchHandlers(actions) {
561
+ return actions.flatMap(
562
+ (a) => a.kind === "dispatch" && a.onSelect ? [[a.id, a.onSelect]] : a.kind === "group" ? collectDispatchHandlers(a.children) : []
563
+ );
564
+ }
556
565
  function toContribution(input) {
557
566
  if (input.mode === SIDEBAR_MODE.none) {
558
567
  return { mode: SIDEBAR_MODE.none };
@@ -579,7 +588,7 @@ function contributionKey(input) {
579
588
  if (input.mode === SIDEBAR_MODE.none) {
580
589
  return JSON.stringify([SIDEBAR_MODE.none]);
581
590
  }
582
- const actionTuples = input.actions.map((a) => [
591
+ const actionTuple = (a) => [
583
592
  a.id,
584
593
  a.label,
585
594
  a.icon ?? "",
@@ -589,8 +598,11 @@ function contributionKey(input) {
589
598
  a.active ? 1 : 0,
590
599
  a.requiredPermission ?? "",
591
600
  a.kind,
592
- a.kind === "navigate" ? a.href : ""
593
- ]);
601
+ a.kind === "navigate" ? a.href : "",
602
+ // Group children fold into the key so a change to any collapsed leaf forces a republish.
603
+ a.kind === "group" ? a.children.map(actionTuple) : ""
604
+ ];
605
+ const actionTuples = input.actions.map(actionTuple);
594
606
  if (input.mode === SIDEBAR_MODE.actions) {
595
607
  return JSON.stringify([SIDEBAR_MODE.actions, input.entityToken, actionTuples]);
596
608
  }
@@ -623,11 +635,7 @@ function useHostSidebarActions(input) {
623
635
  const compatible = api && isHostChromeCompatible(api.capabilities?.contractVersion) ? api : null;
624
636
  const actions = input.mode === SIDEBAR_MODE.none ? [] : input.actions;
625
637
  const handlers = react.useRef(/* @__PURE__ */ new Map());
626
- handlers.current = new Map(
627
- actions.flatMap(
628
- (a) => a.kind === "dispatch" && a.onSelect ? [[a.id, a.onSelect]] : []
629
- )
630
- );
638
+ handlers.current = new Map(collectDispatchHandlers(actions));
631
639
  react.useEffect(() => {
632
640
  if (!compatible) {
633
641
  reportNoHost(pageId);
@@ -677,37 +685,46 @@ function slugRelative(href, basePath) {
677
685
  function isConfinable(slugRelativeHref) {
678
686
  return slugRelativeHref.length > 0 && !slugRelativeHref.startsWith("/") && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(slugRelativeHref) && !slugRelativeHref.includes("#");
679
687
  }
680
- function flattenLeaves(items) {
681
- const out = [];
682
- for (const item of items) {
683
- if (item.children?.length) out.push(...flattenLeaves(item.children));
684
- else out.push(item);
685
- }
686
- return out;
687
- }
688
688
  function toHostActions(actions, opts) {
689
+ const seen = /* @__PURE__ */ new Set();
689
690
  const out = [];
690
- for (const item of flattenLeaves(actions)) {
691
- const mapped = toHostAction(item, opts);
691
+ for (const item of actions) {
692
+ const mapped = toHostAction(item, opts, seen);
692
693
  if (mapped) out.push(mapped);
693
694
  }
694
- const seen = /* @__PURE__ */ new Set();
695
- return out.filter((a) => seen.has(a.id) ? false : (seen.add(a.id), true));
695
+ return out;
696
696
  }
697
- function toHostAction(item, opts) {
697
+ function toHostAction(item, opts, seen) {
698
+ if (seen.has(item.id)) return null;
698
699
  const slot = item.slot === "primary" ? "primary" : item.slot === "sidebar-only" ? "sidebar-only" : "overflow";
699
700
  const variant = item.variant === "danger" ? "danger" : void 0;
700
701
  const shared = { id: item.id, label: item.label, icon: item.icon, slot, variant, disabled: item.disabled };
702
+ if (item.children?.length) {
703
+ seen.add(item.id);
704
+ const children = [];
705
+ for (const child of item.children) {
706
+ const mapped = toHostAction(child, opts, seen);
707
+ if (mapped) children.push(mapped);
708
+ }
709
+ if (children.length === 0) {
710
+ seen.delete(item.id);
711
+ return null;
712
+ }
713
+ return { ...shared, kind: "group", children };
714
+ }
701
715
  if (item.href) {
702
716
  const href = slugRelative(item.href, opts.basePath);
703
717
  if (!isConfinable(href)) return null;
718
+ seen.add(item.id);
704
719
  return { ...shared, kind: "navigate", href };
705
720
  }
706
721
  if (item.actionId) {
707
722
  const actionId = item.actionId;
723
+ seen.add(item.id);
708
724
  return { ...shared, kind: "dispatch", onSelect: () => opts.onDispatch(actionId) };
709
725
  }
710
726
  if (item.onClick) {
727
+ seen.add(item.id);
711
728
  return { ...shared, kind: "dispatch", onSelect: item.onClick };
712
729
  }
713
730
  return null;