@ethisyscore/plugin-ui 1.100.1 → 1.100.3

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.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","../src/components/layout/breadcrumbs.ts","../src/platform-react/activeNavHref.ts"],"names":["createContext","useContext","stripTrailingSlash"],"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).\n * 2.2.0 added the additive, optional `activeNavHref` on every contribution mode; an older host\n * ignores the extra field (it builds its state field-by-field, never by spreading the\n * contribution), so a plugin declaring it against a 2.0/2.1 host is inert, not broken. */\nexport const HOST_CHROME_CONTRACT_VERSION = \"2.2.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\" } & HostSidebarActiveNav)\n | ({ mode: \"actions\"; entityToken: string; actions: HostSidebarActionDescriptor[] } & HostSidebarActiveNav)\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 } & HostSidebarActiveNav);\n\n/**\n * The optional active-nav declaration carried by EVERY contribution mode (contract ≥2.2.0).\n *\n * `activeNavHref` is a slug-relative href — the SAME vocabulary as nav-item and navigate-action\n * hrefs — naming the sidebar entry the plugin considers active for the current route. The host\n * resolves it to its own nav-item id and highlights that entry; when it is absent, or names\n * nothing the host is rendering, the host's URL election decides exactly as it always has.\n *\n * It exists because the host's election can only light an item whose href PREFIXES the current\n * URL, so a sub-page whose URL does not nest under its section (`/extensions/projects/people/:id`\n * belonging to \"People\" at `/extensions/projects/settings`) lights the module root instead. A\n * plugin's information architecture is not always recoverable from its URL shape, so the plugin\n * says so; {@link resolveActiveNavHref} derives it from the `routeMeta` parent chain the plugin\n * already declares for breadcrumbs.\n *\n * It rides on `mode:\"none\"` too — deliberately. `\"none\"` is what a plugin publishes on an\n * ordinary list route with no CTAs, and that is precisely the route shape whose highlight is most\n * often wrong; a declaration there still contributes no actions and still does not clobber the\n * static manifest.\n */\nexport interface HostSidebarActiveNav {\n activeNavHref?: string;\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/**\n * Mirror of the HOST mount's `deriveEntityToken` (PlatformReactSurfaceMount): the\n * route sub-path relative to the PAGE-surface `basePath` — which is the page\n * route's STATIC leading segments, so a fully-static page (list routes) yields\n * `\"/\"` because there the pathname IS the basePath. The host silently drops a\n * publish whose `entityToken` doesn't match its own derivation, so this must stay\n * bit-identical to the host helper. No basePath (legacy host) falls back to the\n * full pathname, exactly like the host does.\n */\nexport function deriveHostEntityToken(pathname: string, basePath: string | undefined): string {\n if (!basePath) {\n return pathname;\n }\n const base = stripTrailingSlash(basePath);\n if (pathname === base) {\n return \"/\";\n }\n if (pathname.startsWith(`${base}/`)) {\n return pathname.slice(base.length);\n }\n return pathname;\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 HostSidebarActiveNav,\n type HostSidebarContribution,\n type HostSidebarNavItem,\n} from \"./hostSidebarActionsContext\";\nimport { SurfaceBaseContext, deriveHostEntityToken } 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.\n *\n * `entityToken` is REQUIRED here — this is the published shape existing consumers narrow\n * against (`input.entityToken` is a `string` after a mode check), unchanged. To let the\n * hook derive the token instead, OMIT the property via {@link HostSidebarDerivedInput}. */\nexport type HostSidebarInput =\n | ({ mode: \"none\" } & HostSidebarActiveNav)\n | ({ mode: \"actions\"; entityToken: string; actions: HostSidebarAction[] } & HostSidebarActiveNav)\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 } & HostSidebarActiveNav);\n\n/** Token-less authoring variants: the hook derives the host-mirrored entity token from the\n * surface base + current location (`deriveHostEntityToken`) — correct by construction, since\n * the host's token guard silently drops a mismatching publish. Prefer these unless you need\n * the bare-entity-id form (which the host segment-matches across an entity's sub-pages).\n * `entityToken?: undefined` (not just absence) is deliberate: an EXPLICIT `undefined`\n * (e.g. `entityToken: maybeToken` with an unresolved token) keeps the long-standing\n * \"unresolved token → contribute nothing\" semantic and is treated as `mode:\"none\"`, NOT\n * derived — only a truly ABSENT property derives. */\nexport type HostSidebarDerivedInput =\n | ({ mode: \"actions\"; entityToken?: undefined; actions: HostSidebarAction[] } & HostSidebarActiveNav)\n | ({\n mode: \"detail\";\n entityToken?: undefined;\n title: string;\n navSectionLabel: string;\n navItems: HostSidebarNavItem[];\n backHref: string;\n backLabel: string;\n actions: HostSidebarAction[];\n } & HostSidebarActiveNav);\n\n/** What `useHostSidebarActions` accepts: the classic explicit-token shape or a derived-token variant. */\nexport type HostSidebarHookInput = HostSidebarInput | HostSidebarDerivedInput;\n\n/** Input with the entity token resolved (supplied or derived) — the only shape that crosses\n * to the host, whose contribution contract requires the token. */\ntype ResolvedHostSidebarInput = HostSidebarInput;\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: ResolvedHostSidebarInput): HostSidebarContribution {\n // `activeNavHref` is spread only when the plugin declared one, so a contribution from a plugin\n // that says nothing is byte-identical to the pre-2.2.0 shape.\n const activeNav: HostSidebarActiveNav = input.activeNavHref ? { activeNavHref: input.activeNavHref } : {};\n if (input.mode === SIDEBAR_MODE.none) {\n return { mode: SIDEBAR_MODE.none, ...activeNav };\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 ...activeNav,\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 ...activeNav,\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: ResolvedHostSidebarInput): string {\n // The declaration is part of the visible result, so it belongs in the key: without it a\n // navigation that changes ONLY the declared item would not republish and the highlight would\n // stay on the previous page's entry.\n const activeNav = input.activeNavHref ?? \"\";\n if (input.mode === SIDEBAR_MODE.none) {\n return JSON.stringify([SIDEBAR_MODE.none, activeNav]);\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, activeNav]);\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 activeNav,\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: HostSidebarHookInput): void {\n const api = useContext(HostSidebarActionsContext);\n const surfaceBase = useContext(SurfaceBaseContext);\n const pageId = surfaceBase?.pageId; // diagnostic tag (per-surface)\n const compatible: HostSidebarActionsApi | null =\n api && isHostChromeCompatible(api.capabilities?.contractVersion) ? api : null;\n\n // Resolve the entity token. Three cases, in order:\n // 1. Explicit string token → publish as-is (the pre-existing behaviour, unchanged).\n // 2. Property PRESENT but undefined (`entityToken: maybeToken` with an unresolved\n // token) → mode:\"none\". This preserves the long-standing \"unresolved token →\n // contribute nothing\" gate (`toHostSidebarInput` has always mapped an undefined\n // token to none) — deriving here would surface actions for an entity the page\n // has not resolved yet.\n // 3. Property ABSENT → derive the host-mirrored token from the surface base +\n // current location, so the host's (silent) token guard can never drop the\n // publish over a token-shape mismatch. The derived token re-computes on every\n // render; the content key below re-publishes when it changes (a location change\n // also makes the HOST clear its state synchronously, so a republish is required\n // with explicit tokens too — this path is no less reactive than they are).\n const resolved: ResolvedHostSidebarInput =\n input.mode === SIDEBAR_MODE.none\n ? input\n : typeof input.entityToken === \"string\"\n ? (input as ResolvedHostSidebarInput)\n : \"entityToken\" in input\n ? // An unresolved entity token suppresses the CONTRIBUTION, not the page's identity:\n // the sidebar entry the page belongs to is known before its entity has loaded, so the\n // declaration (when there is one) is carried through rather than dropped, and the\n // section stays lit while the detail data is in flight.\n { mode: SIDEBAR_MODE.none, ...(input.activeNavHref ? { activeNavHref: input.activeNavHref } : {}) }\n : {\n ...input,\n entityToken: deriveHostEntityToken(\n typeof window !== \"undefined\" ? window.location.pathname : \"\",\n surfaceBase?.basePath,\n ),\n };\n\n // Ref-map refreshed every render → dispatch always hits the latest handler (recursing groups).\n // Built from RESOLVED (not raw input): when resolution collapses to mode:\"none\" (explicit\n // undefined token), the handlers must be empty too — a stale host dispatch arriving before\n // the none-publish clears the sidebar must not invoke an unresolved-entity action.\n const actions = resolved.mode === SIDEBAR_MODE.none ? [] : resolved.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(resolved);\n useEffect(() => {\n if (!compatible) return;\n compatible.publish(toContribution(resolved));\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","import type { Breadcrumb, RouteMeta, RouteMetaEntry } from \"./pageHeaderTypes\";\n\n/** Synthesised root crumb — mirrors the monolith's virtual `{ pattern: \"/\", label: \"Home\" }`. */\nconst HOME_CRUMB: Breadcrumb = { label: \"Home\", href: \"/\" };\n\nfunction stripTrailingSlash(value: string): string {\n return value.length > 1 && value.endsWith(\"/\") ? value.slice(0, -1) : value;\n}\n\n/**\n * Orders routes most-specific first: more segments win, then at equal depth prefer\n * literal segments over `:param` ones — identical to the monolith's `SORTED_ROUTE_META`.\n */\nexport function sortBySpecificity(routeMeta: RouteMeta): RouteMetaEntry[] {\n return [...routeMeta].sort((a, b) => {\n const aDepth = a.pattern.split(\"/\").length;\n const bDepth = b.pattern.split(\"/\").length;\n if (bDepth !== aDepth) {\n return bDepth - aDepth;\n }\n const aDynamic = (a.pattern.match(/:/g) ?? []).length;\n const bDynamic = (b.pattern.match(/:/g) ?? []).length;\n return aDynamic - bDynamic;\n });\n}\n\n/**\n * Matches a pathname against the sorted routes segment-by-segment, capturing\n * `:param` values. Returns the first (most-specific) match, or `null`. Port of the\n * monolith's `matchRoute`.\n */\nexport function matchRoute(\n pathname: string,\n sorted: RouteMetaEntry[],\n): { entry: RouteMetaEntry; params: Record<string, string> } | null {\n const pathSegments = stripTrailingSlash(pathname).split(\"/\");\n for (const entry of sorted) {\n const patternSegments = entry.pattern.split(\"/\");\n if (patternSegments.length !== pathSegments.length) {\n continue;\n }\n const params: Record<string, string> = {};\n let matched = true;\n for (let i = 0; i < patternSegments.length; i++) {\n const pSeg = patternSegments[i];\n const vSeg = pathSegments[i];\n if (pSeg.startsWith(\":\")) {\n params[pSeg.slice(1)] = vSeg;\n } else if (pSeg !== vSeg) {\n matched = false;\n break;\n }\n }\n if (matched) {\n return { entry, params };\n }\n }\n return null;\n}\n\n/**\n * Replaces `:param` segments in a pattern with captured values. Port of\n * `resolvePattern`. The param-name class matches the identifiers `matchRoute`\n * captures (letters/digits/underscore, e.g. `:user_id`, `:id2`); when a value is\n * missing the original `:param` token is preserved rather than dropping the colon.\n */\nfunction resolvePattern(pattern: string, params: Record<string, string>): string {\n return pattern.replace(/:([A-Za-z0-9_]+)/g, (_, name: string) => params[name] ?? `:${name}`);\n}\n\n/**\n * Builds the `Home / … / <page>` breadcrumb trail — a faithful port of the\n * monolith's `buildBreadcrumbs`. Matches the current path against the `routeMeta`\n * patterns (most-specific wins), walks the `parent` chain, prepends Home, and maps\n * each ancestor to its resolved-pattern href (the current page gets no href). The\n * app/module crumb (e.g. \"Finance\") is just the top-level `routeMeta` entry — no\n * separate config, exactly like the monolith's `/finance` root entry.\n */\nexport function buildBreadcrumbs(pathname: string, routeMeta: RouteMeta): Breadcrumb[] {\n const match = matchRoute(pathname, sortBySpecificity(routeMeta));\n if (match == null) {\n return [HOME_CRUMB];\n }\n\n const { entry: current, params } = match;\n\n // Walk the parent chain (leaf → root), guarding against cycles.\n const chain: RouteMetaEntry[] = [current];\n const seen = new Set<string>([current.pattern]);\n let node = current;\n while (node.parent != null) {\n const parent = routeMeta.find((e) => e.pattern === node.parent);\n if (parent == null || seen.has(parent.pattern)) {\n break;\n }\n seen.add(parent.pattern);\n chain.unshift(parent);\n node = parent;\n }\n\n const crumbs: Breadcrumb[] = [HOME_CRUMB];\n chain.forEach((entry, index) => {\n const isLast = index === chain.length - 1;\n crumbs.push(isLast ? { label: entry.label } : { label: entry.label, href: resolvePattern(entry.pattern, params) });\n });\n return crumbs;\n}\n","import { matchRoute, sortBySpecificity } from \"../components/layout/breadcrumbs\";\nimport type { RouteMeta, RouteMetaEntry } from \"../components/layout/pageHeaderTypes\";\nimport type { HostSidebarHookInput } from \"./useHostSidebarActions\";\n\n/**\n * Options for {@link resolveActiveNavHref}.\n */\nexport interface ResolveActiveNavHrefOptions {\n /**\n * The plugin's mount base (e.g. `/extensions/projects`), stripped to make the result\n * slug-relative for the host confiner. Defaults to the `/extensions/<slug>` prefix of the\n * pathname — the same default {@link menuConfigToHostSidebarInput} uses.\n */\n basePath?: string;\n}\n\n/** A pattern with no `:param` segments resolves to exactly one concrete URL, so it can name a\n * nav item. A parameterised one cannot: it stands for a family of URLs, not a menu entry. */\nfunction isConcrete(pattern: string): boolean {\n return !pattern.split(\"/\").some((seg) => seg.startsWith(\":\"));\n}\n\n/**\n * Derive the sidebar entry that owns the current route, as a slug-relative href for\n * `activeNavHref` on a `useHostSidebarActions` input.\n *\n * The source of truth is the plugin's own `routeMeta` — the `{ pattern, label, parent }` chain it\n * already declares for breadcrumbs. That chain IS the plugin's information architecture: it\n * records that `/extensions/projects/people/:id` belongs under `/extensions/projects/settings`,\n * which no amount of URL inspection can recover, because the two share no path. So this walks the\n * matched route up its `parent` chain and returns the nearest CONCRETE (param-free) ancestor:\n *\n * - `/extensions/projects/people/abc` → `settings` (via parent `/extensions/projects/settings`)\n * - `/extensions/tech/technologies/7/edit` → `technologies` (two parents up, past `…/:id`)\n * - `/extensions/projects/all` → `all` (the route is its own nav item)\n *\n * Returns `undefined` — meaning \"no opinion, let the host's URL election decide\" — when the path\n * matches no route, when no ancestor is concrete, or when the answer is the plugin ROOT. The root\n * is deliberately not declarable: its href prefixes every URL in the plugin, so the URL election\n * already picks it whenever nothing deeper matches, and there is no case where declaring it\n * changes an outcome.\n *\n * The result is a HINT. The host validates it against the nav items it is actually rendering and\n * falls back to its URL election if it names nothing, so a stale or mistaken `routeMeta` entry\n * degrades to today's behaviour rather than blanking the sidebar.\n */\nexport function resolveActiveNavHref(\n pathname: string,\n routeMeta: RouteMeta,\n opts: ResolveActiveNavHrefOptions = {},\n): string | undefined {\n const base = opts.basePath ?? /^\\/extensions\\/[^/]+/.exec(pathname)?.[0];\n if (!base) return undefined;\n const mountBase = base.endsWith(\"/\") ? base.slice(0, -1) : base;\n\n const match = matchRoute(pathname, sortBySpecificity(routeMeta));\n if (match == null) return undefined;\n\n // Walk leaf → root, guarding against a cyclic `parent` chain exactly as `buildBreadcrumbs` does.\n const seen = new Set<string>([match.entry.pattern]);\n let node: RouteMetaEntry | undefined = match.entry;\n while (node != null) {\n if (isConcrete(node.pattern)) {\n const owner = node.pattern.replace(/\\/$/, \"\");\n if (owner === mountBase) return undefined; // the plugin root — nothing to declare\n const prefix = `${mountBase}/`;\n return owner.startsWith(prefix) ? owner.slice(prefix.length) : undefined;\n }\n const parentPattern: string | undefined = node.parent;\n if (parentPattern == null || seen.has(parentPattern)) return undefined;\n seen.add(parentPattern);\n node = routeMeta.find((e) => e.pattern === parentPattern);\n }\n return undefined;\n}\n\n/**\n * Attach an active-nav declaration to an existing `useHostSidebarActions` input.\n *\n * The input union is discriminated on `mode`, so a plugin cannot just spread the field on without\n * re-narrowing; this does it once, correctly, for every mode — including `mode:\"none\"`, which is\n * what the input builders return on an ordinary list route and is exactly where the declaration\n * matters most. Passing `undefined` returns the input untouched, so the call site needs no branch.\n *\n * ```ts\n * const input = withActiveNavHref(\n * menuConfigToHostSidebarInput(menuConfig, { pathname, onDispatch }),\n * resolveActiveNavHref(pathname, routeMeta),\n * );\n * useHostSidebarActions(input);\n * ```\n */\nexport function withActiveNavHref(\n input: HostSidebarHookInput,\n activeNavHref: string | undefined,\n): HostSidebarHookInput {\n if (!activeNavHref) return input;\n return { ...input, activeNavHref };\n}\n"]}
1
+ {"version":3,"sources":["../src/currentUser.ts","../src/platform-react/hostSidebarActionsContext.ts","../src/platform-react/surfaceUrl.ts","../src/platform-react/useHostSidebarActions.ts","../src/components/layout/breadcrumbs.ts","../src/platform-react/activeNavHref.ts"],"names":["createContext","useContext","stripTrailingSlash"],"mappings":";;;;;;AAiDO,SAAS,cAAA,GAAqC;AACnD,EAAA,MAAM,WAAW,eAAA,EAAgB;AACjC,EAAA,MAAM,IAAA,GAAO,UAAU,IAAA,IAAQ,IAAA;AAC/B,EAAA,IAAI,CAAC,IAAA,EAAM;AACT,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,OAAO;AAAA,IACL,IAAI,IAAA,CAAK,EAAA;AAAA;AAAA;AAAA,IAGT,WAAA,EACE,IAAA,CAAK,QAAA,EAAU,IAAA,MAAU,CAAA,EAAG,IAAA,CAAK,SAAA,IAAa,EAAE,CAAA,CAAA,EAAI,IAAA,CAAK,QAAA,IAAY,EAAE,GAAG,IAAA;AAAK,GACnF;AACF;ACtDA,IAAM,WAAA,mBAAc,MAAA,CAAO,GAAA,CAAI,iDAAiD,CAAA;AAChF,IAAM,QAAA,mBAAW,MAAA,CAAO,GAAA,CAAI,gDAAgD,CAAA;AASrE,IAAM,4BAAA,GAA+B;AAkH5C,IAAM,EAAA,GAAK,UAAA;AACJ,IAAM,yBAAA,GACX,GAAG,WAAW,CAAA,KAAM,GAAG,WAAW,CAAA,GAAI,cAA4C,IAAI,CAAA;AAgBxF,IAAM,GAAA,GAAM,UAAA;AACZ,IAAI,GAAA,CAAI,QAAQ,CAAA,KAAM,MAAA,EAAW;AAC/B,EAAA,GAAA,CAAI,QAAQ,IAAI,MAAM;AAAA,EAAC,CAAA;AACzB;AACO,SAAS,4BAA4B,IAAA,EAA8B;AACxE,EAAA,GAAA,CAAI,QAAQ,CAAA,GAAI,IAAA;AAClB;AACO,SAAS,2BAA2B,CAAA,EAA+B;AACxE,EAAA,IAAI;AACF,IAAA,GAAA,CAAI,QAAQ,EAAG,CAAC,CAAA;AAAA,EAClB,CAAA,CAAA,MAAQ;AAAA,EAER;AACF;AAGO,SAAS,uBAAuB,WAAA,EAA0C;AAC/E,EAAA,IAAI,WAAA,KAAgB,QAAW,OAAO,KAAA;AACtC,EAAA,MAAM,SAAA,GAAY,MAAA,CAAO,QAAA,CAAS,WAAA,CAAY,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAA,IAAK,EAAA,EAAI,EAAE,CAAA;AACrE,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,QAAA,CAAS,4BAAA,CAA6B,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAA,IAAK,EAAA,EAAI,EAAE,CAAA;AACrF,EAAA,IAAI,MAAA,CAAO,MAAM,SAAS,CAAA,IAAK,OAAO,KAAA,CAAM,QAAQ,GAAG,OAAO,KAAA;AAC9D,EAAA,OAAO,SAAA,KAAc,QAAA;AACvB;ACjKO,IAAM,kBAAA,GAAqBA,cAA4C,MAAS,CAAA;AAOvF,SAAS,mBAAmB,CAAA,EAAmB;AAC7C,EAAA,OAAO,CAAA,CAAE,MAAA,GAAS,CAAA,IAAK,CAAA,CAAE,QAAA,CAAS,GAAG,CAAA,GAAI,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,GAAI,CAAA;AAC5D;AAoCO,SAAS,qBAAA,CAAsB,UAAkB,QAAA,EAAsC;AAC5F,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,MAAM,IAAA,GAAO,mBAAmB,QAAQ,CAAA;AACxC,EAAA,IAAI,aAAa,IAAA,EAAM;AACrB,IAAA,OAAO,GAAA;AAAA,EACT;AACA,EAAA,IAAI,QAAA,CAAS,UAAA,CAAW,CAAA,EAAG,IAAI,GAAG,CAAA,EAAG;AACnC,IAAA,OAAO,QAAA,CAAS,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA;AAAA,EACnC;AACA,EAAA,OAAO,QAAA;AACT;;;ACtCO,IAAM,eAAe,EAAE,IAAA,EAAM,QAAQ,OAAA,EAAS,SAAA,EAAW,QAAQ,QAAA;AAoDxE,SAAS,aAAa,CAAA,EAAmD;AACvE,EAAA,IAAI,CAAA,CAAE,SAAS,UAAA,EAAY;AACzB,IAAA,MAAM,EAAE,QAAA,EAAU,KAAA,EAAO,GAAG,MAAK,GAAI,CAAA;AACrC,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,CAAA,CAAE,SAAS,OAAA,EAAS;AAEtB,IAAA,MAAM,EAAE,QAAA,EAAU,GAAG,IAAA,EAAK,GAAI,CAAA;AAC9B,IAAA,OAAO,EAAE,GAAG,IAAA,EAAM,UAAU,QAAA,CAAS,GAAA,CAAI,YAAY,CAAA,EAAE;AAAA,EACzD;AACA,EAAA,OAAO,CAAA;AACT;AAIA,SAAS,wBAAwB,OAAA,EAAoE;AACnG,EAAA,OAAO,OAAA,CAAQ,OAAA;AAAA,IAAQ,CAAC,MACtB,CAAA,CAAE,IAAA,KAAS,cAAc,CAAA,CAAE,QAAA,GACvB,CAAC,CAAC,CAAA,CAAE,EAAA,EAAI,EAAE,QAAQ,CAAU,IAC5B,CAAA,CAAE,IAAA,KAAS,UACT,uBAAA,CAAwB,CAAA,CAAE,QAAQ,CAAA,GAClC;AAAC,GACT;AACF;AAIA,SAAS,eAAe,KAAA,EAA0D;AAGhF,EAAA,MAAM,SAAA,GAAkC,MAAM,aAAA,GAAgB,EAAE,eAAe,KAAA,CAAM,aAAA,KAAkB,EAAC;AACxG,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,YAAA,CAAa,IAAA,EAAM;AACpC,IAAA,OAAO,EAAE,IAAA,EAAM,YAAA,CAAa,IAAA,EAAM,GAAG,SAAA,EAAU;AAAA,EACjD;AACA,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,YAAA,CAAa,OAAA,EAAS;AACvC,IAAA,OAAO;AAAA,MACL,MAAM,YAAA,CAAa,OAAA;AAAA,MACnB,aAAa,KAAA,CAAM,WAAA;AAAA,MACnB,OAAA,EAAS,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,YAAY,CAAA;AAAA,MACvC,GAAG;AAAA,KACL;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,MAAM,YAAA,CAAa,MAAA;AAAA,IACnB,aAAa,KAAA,CAAM,WAAA;AAAA,IACnB,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,iBAAiB,KAAA,CAAM,eAAA;AAAA,IACvB,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,WAAW,KAAA,CAAM,SAAA;AAAA,IACjB,OAAA,EAAS,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,YAAY,CAAA;AAAA,IACvC,GAAG;AAAA,GACL;AACF;AAQA,SAAS,eAAA,CAAgB,OAAiC,SAAA,EAA2B;AAInF,EAAA,MAAM,SAAA,GAAY,MAAM,aAAA,IAAiB,EAAA;AACzC,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,YAAA,CAAa,IAAA,EAAM;AAepC,IAAA,OAAO,IAAA,CAAK,SAAA,CAAU,SAAA,GAAY,CAAC,YAAA,CAAa,IAAA,EAAM,SAAA,EAAW,SAAS,CAAA,GAAI,CAAC,YAAA,CAAa,IAAA,EAAM,SAAS,CAAC,CAAA;AAAA,EAC9G;AACA,EAAA,MAAM,WAAA,GAAc,CAAC,CAAA,KAAoC;AAAA,IACvD,CAAA,CAAE,EAAA;AAAA,IACF,CAAA,CAAE,KAAA;AAAA,IACF,EAAE,IAAA,IAAQ,EAAA;AAAA,IACV,EAAE,IAAA,IAAQ,EAAA;AAAA,IACV,EAAE,OAAA,IAAW,EAAA;AAAA,IACb,CAAA,CAAE,WAAW,CAAA,GAAI,CAAA;AAAA,IACjB,CAAA,CAAE,SAAS,CAAA,GAAI,CAAA;AAAA,IACf,EAAE,kBAAA,IAAsB,EAAA;AAAA,IACxB,CAAA,CAAE,IAAA;AAAA,IACF,CAAA,CAAE,IAAA,KAAS,UAAA,GAAa,CAAA,CAAE,IAAA,GAAO,EAAA;AAAA;AAAA,IAEjC,EAAE,IAAA,KAAS,OAAA,GAAU,EAAE,QAAA,CAAS,GAAA,CAAI,WAAW,CAAA,GAAI;AAAA,GACrD;AACA,EAAA,MAAM,YAAA,GAAe,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA;AAClD,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,YAAA,CAAa,OAAA,EAAS;AACvC,IAAA,OAAO,IAAA,CAAK,UAAU,CAAC,YAAA,CAAa,SAAS,KAAA,CAAM,WAAA,EAAa,YAAA,EAAc,SAAS,CAAC,CAAA;AAAA,EAC1F;AAEA,EAAA,MAAM,YAAY,KAAA,CAAM,QAAA,CAAS,GAAA,CAAI,CAAC,MAAM,CAAC,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,OAAO,CAAA,CAAE,IAAA,IAAQ,EAAA,EAAI,CAAA,CAAE,IAAI,CAAC,CAAA;AACjF,EAAA,OAAO,KAAK,SAAA,CAAU;AAAA,IACpB,YAAA,CAAa,MAAA;AAAA,IACb,KAAA,CAAM,WAAA;AAAA,IACN,KAAA,CAAM,KAAA;AAAA,IACN,KAAA,CAAM,eAAA;AAAA,IACN,KAAA,CAAM,QAAA;AAAA,IACN,KAAA,CAAM,SAAA;AAAA,IACN,SAAA;AAAA,IACA,YAAA;AAAA,IACA;AAAA,GACD,CAAA;AACH;AAIA,IAAM,cAAA,uBAAqB,GAAA,EAAY;AACvC,SAAS,aAAa,MAAA,EAAkC;AAEtD,EAAA,0BAAA,CAA2B,EAAE,KAAA,EAAO,kCAAA,EAAoC,MAAA,EAAQ,CAAA;AAChF,EAAA,MAAM,MAAM,MAAA,IAAU,GAAA;AAEtB,EAAA,IAAK,YAAoB,GAAA,EAAK,GAAA,IAAO,CAAC,cAAA,CAAe,GAAA,CAAI,GAAG,CAAA,EAAG;AAC7D,IAAA,cAAA,CAAe,IAAI,GAAG,CAAA;AAEtB,IAAA,OAAA,CAAQ,IAAA;AAAA,MACN,sCAAsC,GAAG,CAAA,yIAAA;AAAA,KAE3C;AAAA,EACF;AACF;AAYO,SAAS,sBAAsB,KAAA,EAAmC;AACvE,EAAA,MAAM,GAAA,GAAMC,WAAW,yBAAyB,CAAA;AAChD,EAAA,MAAM,WAAA,GAAcA,WAAW,kBAAkB,CAAA;AACjD,EAAA,MAAM,SAAS,WAAA,EAAa,MAAA;AAC5B,EAAA,MAAM,aACJ,GAAA,IAAO,sBAAA,CAAuB,IAAI,YAAA,EAAc,eAAe,IAAI,GAAA,GAAM,IAAA;AAe3E,EAAA,MAAM,QAAA,GACJ,KAAA,CAAM,IAAA,KAAS,YAAA,CAAa,IAAA,GACxB,KAAA,GACA,OAAO,KAAA,CAAM,WAAA,KAAgB,QAAA,GAC1B,KAAA,GACD,aAAA,IAAiB,KAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKf,EAAE,IAAA,EAAM,YAAA,CAAa,IAAA,EAAM,GAAI,KAAA,CAAM,aAAA,GAAgB,EAAE,aAAA,EAAe,KAAA,CAAM,aAAA,EAAc,GAAI,EAAC;AAAG,MAClG;AAAA,IACE,GAAG,KAAA;AAAA,IACH,WAAA,EAAa,qBAAA;AAAA,MACX,OAAO,MAAA,KAAW,WAAA,GAAc,MAAA,CAAO,SAAS,QAAA,GAAW,EAAA;AAAA,MAC3D,WAAA,EAAa;AAAA;AACf,GACF;AAMV,EAAA,MAAM,UAAU,QAAA,CAAS,IAAA,KAAS,aAAa,IAAA,GAAO,KAAK,QAAA,CAAS,OAAA;AACpE,EAAA,MAAM,QAAA,GAAW,MAAA,iBAAgC,IAAI,GAAA,EAAK,CAAA;AAC1D,EAAA,QAAA,CAAS,OAAA,GAAU,IAAI,GAAA,CAAI,uBAAA,CAAwB,OAAO,CAAC,CAAA;AAE3D,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,UAAA,EAAY;AACf,MAAA,YAAA,CAAa,MAAM,CAAA;AACnB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,SAAA,CAAU,CAAC,EAAA,KAAO;AACzC,MAAA,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA,IAAI;AAAA,IAC7B,CAAC,CAAA;AACD,IAAA,OAAO,MAAM;AACX,MAAA,UAAA,CAAW,KAAA,EAAM;AACjB,MAAA,KAAA,EAAM;AAAA,IACR,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,UAAA,EAAY,MAAM,CAAC,CAAA;AAOvB,EAAA,MAAM,GAAA,GAAM,eAAA;AAAA,IACV,QAAA;AAAA,IACA,qBAAA;AAAA,MACE,OAAO,MAAA,KAAW,WAAA,GAAc,MAAA,CAAO,SAAS,QAAA,GAAW,EAAA;AAAA,MAC3D,WAAA,EAAa;AAAA;AACf,GACF;AACA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,UAAA,EAAY;AACjB,IAAA,UAAA,CAAW,OAAA,CAAQ,cAAA,CAAe,QAAQ,CAAC,CAAA;AAAA,EAG7C,CAAA,EAAG,CAAC,UAAA,EAAY,GAAG,CAAC,CAAA;AACtB;;;AC7SA,SAASC,oBAAmB,KAAA,EAAuB;AACjD,EAAA,OAAO,KAAA,CAAM,MAAA,GAAS,CAAA,IAAK,KAAA,CAAM,QAAA,CAAS,GAAG,CAAA,GAAI,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,GAAI,KAAA;AACxE;AAMO,SAAS,kBAAkB,SAAA,EAAwC;AACxE,EAAA,OAAO,CAAC,GAAG,SAAS,EAAE,IAAA,CAAK,CAAC,GAAG,CAAA,KAAM;AACnC,IAAA,MAAM,MAAA,GAAS,CAAA,CAAE,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA,CAAE,MAAA;AACpC,IAAA,MAAM,MAAA,GAAS,CAAA,CAAE,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA,CAAE,MAAA;AACpC,IAAA,IAAI,WAAW,MAAA,EAAQ;AACrB,MAAA,OAAO,MAAA,GAAS,MAAA;AAAA,IAClB;AACA,IAAA,MAAM,YAAY,CAAA,CAAE,OAAA,CAAQ,MAAM,IAAI,CAAA,IAAK,EAAC,EAAG,MAAA;AAC/C,IAAA,MAAM,YAAY,CAAA,CAAE,OAAA,CAAQ,MAAM,IAAI,CAAA,IAAK,EAAC,EAAG,MAAA;AAC/C,IAAA,OAAO,QAAA,GAAW,QAAA;AAAA,EACpB,CAAC,CAAA;AACH;AAOO,SAAS,UAAA,CACd,UACA,MAAA,EACkE;AAClE,EAAA,MAAM,YAAA,GAAeA,mBAAAA,CAAmB,QAAQ,CAAA,CAAE,MAAM,GAAG,CAAA;AAC3D,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,IAAA,MAAM,eAAA,GAAkB,KAAA,CAAM,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA;AAC/C,IAAA,IAAI,eAAA,CAAgB,MAAA,KAAW,YAAA,CAAa,MAAA,EAAQ;AAClD,MAAA;AAAA,IACF;AACA,IAAA,MAAM,SAAiC,EAAC;AACxC,IAAA,IAAI,OAAA,GAAU,IAAA;AACd,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,eAAA,CAAgB,QAAQ,CAAA,EAAA,EAAK;AAC/C,MAAA,MAAM,IAAA,GAAO,gBAAgB,CAAC,CAAA;AAC9B,MAAA,MAAM,IAAA,GAAO,aAAa,CAAC,CAAA;AAC3B,MAAA,IAAI,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,EAAG;AACxB,QAAA,MAAA,CAAO,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,CAAA,GAAI,IAAA;AAAA,MAC1B,CAAA,MAAA,IAAW,SAAS,IAAA,EAAM;AACxB,QAAA,OAAA,GAAU,KAAA;AACV,QAAA;AAAA,MACF;AAAA,IACF;AACA,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,OAAO,EAAE,OAAO,MAAA,EAAO;AAAA,IACzB;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;;;AC9BA,IAAM,kBAAA,GAAqB,wCAAA;AAI3B,SAAS,WAAW,OAAA,EAA0B;AAC5C,EAAA,OAAO,CAAC,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA,CAAE,IAAA,CAAK,CAAC,GAAA,KAAQ,GAAA,CAAI,UAAA,CAAW,GAAG,CAAC,CAAA;AAC9D;AA+BO,SAAS,oBAAA,CACd,QAAA,EACA,SAAA,EACA,IAAA,GAAoC,EAAC,EACjB;AACpB,EAAA,MAAM,OAAO,IAAA,CAAK,QAAA,IAAY,mBAAmB,IAAA,CAAK,QAAQ,IAAI,CAAC,CAAA;AACnE,EAAA,IAAI,CAAC,MAAM,OAAO,MAAA;AAClB,EAAA,MAAM,SAAA,GAAY,KAAK,QAAA,CAAS,GAAG,IAAI,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,GAAI,IAAA;AAE3D,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,QAAA,EAAU,iBAAA,CAAkB,SAAS,CAAC,CAAA;AAC/D,EAAA,IAAI,KAAA,IAAS,MAAM,OAAO,MAAA;AAG1B,EAAA,MAAM,uBAAO,IAAI,GAAA,CAAY,CAAC,KAAA,CAAM,KAAA,CAAM,OAAO,CAAC,CAAA;AAClD,EAAA,IAAI,OAAmC,KAAA,CAAM,KAAA;AAC7C,EAAA,OAAO,QAAQ,IAAA,EAAM;AACnB,IAAA,IAAI,UAAA,CAAW,IAAA,CAAK,OAAO,CAAA,EAAG;AAC5B,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC5C,MAAA,IAAI,KAAA,KAAU,WAAW,OAAO,MAAA;AAChC,MAAA,MAAM,MAAA,GAAS,GAAG,SAAS,CAAA,CAAA,CAAA;AAC3B,MAAA,OAAO,KAAA,CAAM,WAAW,MAAM,CAAA,GAAI,MAAM,KAAA,CAAM,MAAA,CAAO,MAAM,CAAA,GAAI,MAAA;AAAA,IACjE;AACA,IAAA,MAAM,gBAAoC,IAAA,CAAK,MAAA;AAC/C,IAAA,IAAI,iBAAiB,IAAA,IAAQ,IAAA,CAAK,GAAA,CAAI,aAAa,GAAG,OAAO,MAAA;AAC7D,IAAA,IAAA,CAAK,IAAI,aAAa,CAAA;AACtB,IAAA,IAAA,GAAO,UAAU,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,YAAY,aAAa,CAAA;AAAA,EAC1D;AACA,EAAA,OAAO,MAAA;AACT;AAkBO,SAAS,iBAAA,CACd,OACA,aAAA,EACsB;AACtB,EAAA,IAAI,CAAC,eAAe,OAAO,KAAA;AAC3B,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,aAAA,EAAc;AACnC","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).\n * 2.2.0 added the additive, optional `activeNavHref` on every contribution mode; an older host\n * ignores the extra field (it builds its state field-by-field, never by spreading the\n * contribution), so a plugin declaring it against a 2.0/2.1 host is inert, not broken. */\nexport const HOST_CHROME_CONTRACT_VERSION = \"2.2.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\" } & HostSidebarActiveNav)\n | ({ mode: \"actions\"; entityToken: string; actions: HostSidebarActionDescriptor[] } & HostSidebarActiveNav)\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 } & HostSidebarActiveNav);\n\n/**\n * The optional active-nav declaration carried by EVERY contribution mode (contract ≥2.2.0).\n *\n * `activeNavHref` is a slug-relative href — the SAME vocabulary as nav-item and navigate-action\n * hrefs — naming the sidebar entry the plugin considers active for the current route. The host\n * resolves it to its own nav-item id and highlights that entry; when it is absent, or names\n * nothing the host is rendering, the host's URL election decides exactly as it always has.\n *\n * It exists because the host's election can only light an item whose href PREFIXES the current\n * URL, so a sub-page whose URL does not nest under its section (`/extensions/projects/people/:id`\n * belonging to \"People\" at `/extensions/projects/settings`) lights the module root instead. A\n * plugin's information architecture is not always recoverable from its URL shape, so the plugin\n * says so; {@link resolveActiveNavHref} derives it from the `routeMeta` parent chain the plugin\n * already declares for breadcrumbs.\n *\n * It rides on `mode:\"none\"` too — deliberately. `\"none\"` is what a plugin publishes on an\n * ordinary list route with no CTAs, and that is precisely the route shape whose highlight is most\n * often wrong; a declaration there still contributes no actions and still does not clobber the\n * static manifest.\n */\nexport interface HostSidebarActiveNav {\n activeNavHref?: string;\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/**\n * Mirror of the HOST mount's `deriveEntityToken` (PlatformReactSurfaceMount): the\n * route sub-path relative to the PAGE-surface `basePath` — which is the page\n * route's STATIC leading segments, so a fully-static page (list routes) yields\n * `\"/\"` because there the pathname IS the basePath. The host silently drops a\n * publish whose `entityToken` doesn't match its own derivation, so this must stay\n * bit-identical to the host helper. No basePath (legacy host) falls back to the\n * full pathname, exactly like the host does.\n */\nexport function deriveHostEntityToken(pathname: string, basePath: string | undefined): string {\n if (!basePath) {\n return pathname;\n }\n const base = stripTrailingSlash(basePath);\n if (pathname === base) {\n return \"/\";\n }\n if (pathname.startsWith(`${base}/`)) {\n return pathname.slice(base.length);\n }\n return pathname;\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 HostSidebarActiveNav,\n type HostSidebarContribution,\n type HostSidebarNavItem,\n} from \"./hostSidebarActionsContext\";\nimport { SurfaceBaseContext, deriveHostEntityToken } 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.\n *\n * `entityToken` is REQUIRED here — this is the published shape existing consumers narrow\n * against (`input.entityToken` is a `string` after a mode check), unchanged. To let the\n * hook derive the token instead, OMIT the property via {@link HostSidebarDerivedInput}. */\nexport type HostSidebarInput =\n | ({ mode: \"none\" } & HostSidebarActiveNav)\n | ({ mode: \"actions\"; entityToken: string; actions: HostSidebarAction[] } & HostSidebarActiveNav)\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 } & HostSidebarActiveNav);\n\n/** Token-less authoring variants: the hook derives the host-mirrored entity token from the\n * surface base + current location (`deriveHostEntityToken`) — correct by construction, since\n * the host's token guard silently drops a mismatching publish. Prefer these unless you need\n * the bare-entity-id form (which the host segment-matches across an entity's sub-pages).\n * `entityToken?: undefined` (not just absence) is deliberate: an EXPLICIT `undefined`\n * (e.g. `entityToken: maybeToken` with an unresolved token) keeps the long-standing\n * \"unresolved token → contribute nothing\" semantic and is treated as `mode:\"none\"`, NOT\n * derived — only a truly ABSENT property derives. */\nexport type HostSidebarDerivedInput =\n | ({ mode: \"actions\"; entityToken?: undefined; actions: HostSidebarAction[] } & HostSidebarActiveNav)\n | ({\n mode: \"detail\";\n entityToken?: undefined;\n title: string;\n navSectionLabel: string;\n navItems: HostSidebarNavItem[];\n backHref: string;\n backLabel: string;\n actions: HostSidebarAction[];\n } & HostSidebarActiveNav);\n\n/** What `useHostSidebarActions` accepts: the classic explicit-token shape or a derived-token variant. */\nexport type HostSidebarHookInput = HostSidebarInput | HostSidebarDerivedInput;\n\n/** Input with the entity token resolved (supplied or derived) — the only shape that crosses\n * to the host, whose contribution contract requires the token. */\ntype ResolvedHostSidebarInput = HostSidebarInput;\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: ResolvedHostSidebarInput): HostSidebarContribution {\n // `activeNavHref` is spread only when the plugin declared one, so a contribution from a plugin\n // that says nothing is byte-identical to the pre-2.2.0 shape.\n const activeNav: HostSidebarActiveNav = input.activeNavHref ? { activeNavHref: input.activeNavHref } : {};\n if (input.mode === SIDEBAR_MODE.none) {\n return { mode: SIDEBAR_MODE.none, ...activeNav };\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 ...activeNav,\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 ...activeNav,\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: ResolvedHostSidebarInput, hostToken: string): string {\n // The declaration is part of the visible result, so it belongs in the key: without it a\n // navigation that changes ONLY the declared item would not republish and the highlight would\n // stay on the previous page's entry.\n const activeNav = input.activeNavHref ?? \"\";\n if (input.mode === SIDEBAR_MODE.none) {\n // A DECLARING `mode:\"none\"` also keys on the host's entity token, which the other two modes\n // already carry through `input.entityToken`. The host clears its whole sidebar state\n // synchronously whenever that token changes (`setCurrentToken`), and it must: one mount\n // instance is reused across page ids, so a declaration that survived the clear would light\n // the previous PAGE's entry on the next one. But the plugin has no reason to re-send a\n // declaration that has not changed, so without the token here the key stayed constant across\n // an entity change, no republish happened, and the section went dark on a sibling entity\n // (e.g. `…/fix-detail/1` -> `…/fix-detail/2`). Keying on it makes the plugin republish\n // exactly when the host discards.\n //\n // Included ONLY when there IS a declaration: an undeclared `mode:\"none\"` keeps its constant\n // key and so republishes on nothing, exactly as before. That matters — its publish is a\n // `setState(EMPTY)` that notifies every subscriber, so keying it on the location would add a\n // host re-render to every navigation of every plugin that declares nothing.\n return JSON.stringify(activeNav ? [SIDEBAR_MODE.none, activeNav, hostToken] : [SIDEBAR_MODE.none, activeNav]);\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, activeNav]);\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 activeNav,\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: HostSidebarHookInput): void {\n const api = useContext(HostSidebarActionsContext);\n const surfaceBase = useContext(SurfaceBaseContext);\n const pageId = surfaceBase?.pageId; // diagnostic tag (per-surface)\n const compatible: HostSidebarActionsApi | null =\n api && isHostChromeCompatible(api.capabilities?.contractVersion) ? api : null;\n\n // Resolve the entity token. Three cases, in order:\n // 1. Explicit string token → publish as-is (the pre-existing behaviour, unchanged).\n // 2. Property PRESENT but undefined (`entityToken: maybeToken` with an unresolved\n // token) → mode:\"none\". This preserves the long-standing \"unresolved token →\n // contribute nothing\" gate (`toHostSidebarInput` has always mapped an undefined\n // token to none) — deriving here would surface actions for an entity the page\n // has not resolved yet.\n // 3. Property ABSENT → derive the host-mirrored token from the surface base +\n // current location, so the host's (silent) token guard can never drop the\n // publish over a token-shape mismatch. The derived token re-computes on every\n // render; the content key below re-publishes when it changes (a location change\n // also makes the HOST clear its state synchronously, so a republish is required\n // with explicit tokens too — this path is no less reactive than they are).\n const resolved: ResolvedHostSidebarInput =\n input.mode === SIDEBAR_MODE.none\n ? input\n : typeof input.entityToken === \"string\"\n ? (input as ResolvedHostSidebarInput)\n : \"entityToken\" in input\n ? // An unresolved entity token suppresses the CONTRIBUTION, not the page's identity:\n // the sidebar entry the page belongs to is known before its entity has loaded, so the\n // declaration (when there is one) is carried through rather than dropped, and the\n // section stays lit while the detail data is in flight.\n { mode: SIDEBAR_MODE.none, ...(input.activeNavHref ? { activeNavHref: input.activeNavHref } : {}) }\n : {\n ...input,\n entityToken: deriveHostEntityToken(\n typeof window !== \"undefined\" ? window.location.pathname : \"\",\n surfaceBase?.basePath,\n ),\n };\n\n // Ref-map refreshed every render → dispatch always hits the latest handler (recursing groups).\n // Built from RESOLVED (not raw input): when resolution collapses to mode:\"none\" (explicit\n // undefined token), the handlers must be empty too — a stale host dispatch arriving before\n // the none-publish clears the sidebar must not invoke an unresolved-entity action.\n const actions = resolved.mode === SIDEBAR_MODE.none ? [] : resolved.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, plus — for a\n // DECLARING `mode:\"none\"` only — the host-mirrored entity token, which that mode has no\n // `entityToken` field to carry. Derived the same way the token-less authoring variants above\n // derive theirs, and re-read every render: the host mount re-renders on every location change\n // (it reads `useLocation`), so this sees the current path whenever the host's own clear fires.\n const key = contributionKey(\n resolved,\n deriveHostEntityToken(\n typeof window !== \"undefined\" ? window.location.pathname : \"\",\n surfaceBase?.basePath,\n ),\n );\n useEffect(() => {\n if (!compatible) return;\n compatible.publish(toContribution(resolved));\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","import type { Breadcrumb, RouteMeta, RouteMetaEntry } from \"./pageHeaderTypes\";\n\n/** Synthesised root crumb — mirrors the monolith's virtual `{ pattern: \"/\", label: \"Home\" }`. */\nconst HOME_CRUMB: Breadcrumb = { label: \"Home\", href: \"/\" };\n\nfunction stripTrailingSlash(value: string): string {\n return value.length > 1 && value.endsWith(\"/\") ? value.slice(0, -1) : value;\n}\n\n/**\n * Orders routes most-specific first: more segments win, then at equal depth prefer\n * literal segments over `:param` ones — identical to the monolith's `SORTED_ROUTE_META`.\n */\nexport function sortBySpecificity(routeMeta: RouteMeta): RouteMetaEntry[] {\n return [...routeMeta].sort((a, b) => {\n const aDepth = a.pattern.split(\"/\").length;\n const bDepth = b.pattern.split(\"/\").length;\n if (bDepth !== aDepth) {\n return bDepth - aDepth;\n }\n const aDynamic = (a.pattern.match(/:/g) ?? []).length;\n const bDynamic = (b.pattern.match(/:/g) ?? []).length;\n return aDynamic - bDynamic;\n });\n}\n\n/**\n * Matches a pathname against the sorted routes segment-by-segment, capturing\n * `:param` values. Returns the first (most-specific) match, or `null`. Port of the\n * monolith's `matchRoute`.\n */\nexport function matchRoute(\n pathname: string,\n sorted: RouteMetaEntry[],\n): { entry: RouteMetaEntry; params: Record<string, string> } | null {\n const pathSegments = stripTrailingSlash(pathname).split(\"/\");\n for (const entry of sorted) {\n const patternSegments = entry.pattern.split(\"/\");\n if (patternSegments.length !== pathSegments.length) {\n continue;\n }\n const params: Record<string, string> = {};\n let matched = true;\n for (let i = 0; i < patternSegments.length; i++) {\n const pSeg = patternSegments[i];\n const vSeg = pathSegments[i];\n if (pSeg.startsWith(\":\")) {\n params[pSeg.slice(1)] = vSeg;\n } else if (pSeg !== vSeg) {\n matched = false;\n break;\n }\n }\n if (matched) {\n return { entry, params };\n }\n }\n return null;\n}\n\n/**\n * Replaces `:param` segments in a pattern with captured values. Port of\n * `resolvePattern`. The param-name class matches the identifiers `matchRoute`\n * captures (letters/digits/underscore, e.g. `:user_id`, `:id2`); when a value is\n * missing the original `:param` token is preserved rather than dropping the colon.\n */\nfunction resolvePattern(pattern: string, params: Record<string, string>): string {\n return pattern.replace(/:([A-Za-z0-9_]+)/g, (_, name: string) => params[name] ?? `:${name}`);\n}\n\n/**\n * Builds the `Home / … / <page>` breadcrumb trail — a faithful port of the\n * monolith's `buildBreadcrumbs`. Matches the current path against the `routeMeta`\n * patterns (most-specific wins), walks the `parent` chain, prepends Home, and maps\n * each ancestor to its resolved-pattern href (the current page gets no href). The\n * app/module crumb (e.g. \"Finance\") is just the top-level `routeMeta` entry — no\n * separate config, exactly like the monolith's `/finance` root entry.\n */\nexport function buildBreadcrumbs(pathname: string, routeMeta: RouteMeta): Breadcrumb[] {\n const match = matchRoute(pathname, sortBySpecificity(routeMeta));\n if (match == null) {\n return [HOME_CRUMB];\n }\n\n const { entry: current, params } = match;\n\n // Walk the parent chain (leaf → root), guarding against cycles.\n const chain: RouteMetaEntry[] = [current];\n const seen = new Set<string>([current.pattern]);\n let node = current;\n while (node.parent != null) {\n const parent = routeMeta.find((e) => e.pattern === node.parent);\n if (parent == null || seen.has(parent.pattern)) {\n break;\n }\n seen.add(parent.pattern);\n chain.unshift(parent);\n node = parent;\n }\n\n const crumbs: Breadcrumb[] = [HOME_CRUMB];\n chain.forEach((entry, index) => {\n const isLast = index === chain.length - 1;\n crumbs.push(isLast ? { label: entry.label } : { label: entry.label, href: resolvePattern(entry.pattern, params) });\n });\n return crumbs;\n}\n","import { matchRoute, sortBySpecificity } from \"../components/layout/breadcrumbs\";\nimport type { RouteMeta, RouteMetaEntry } from \"../components/layout/pageHeaderTypes\";\nimport type { HostSidebarHookInput } from \"./useHostSidebarActions\";\n\n/**\n * Options for {@link resolveActiveNavHref}.\n */\nexport interface ResolveActiveNavHrefOptions {\n /**\n * The plugin's mount base (e.g. `/extensions/projects`), stripped to make the result\n * slug-relative for the host confiner. Defaults to the mount prefix of the pathname — either\n * `/extensions/<slug>` or `/super-admin/extensions/<slug>`, the two roots the host mounts a\n * plugin surface under. Prefer passing the host-supplied `SurfaceBaseContext.basePath`\n * (resolved via `resolveSurfaceBase`) when the plugin has it: that is authoritative for the\n * page actually rendering, where this default is inferred from the URL.\n */\n basePath?: string;\n}\n\n/**\n * The plugin mount roots, longest first.\n *\n * `/super-admin/extensions/<slug>` has to be tried BEFORE `/extensions/<slug>`: the super-admin\n * URL contains the substring `/extensions/`, so an unanchored tenant-first match would slice the\n * base in the wrong place. Anchoring both at `^` and ordering longest-first makes that\n * impossible. Without the super-admin entry the helper simply returned `undefined` for every\n * super-admin surface, so its declaration never reached the host at all.\n */\nconst MOUNT_BASE_PATTERN = /^(?:\\/super-admin)?\\/extensions\\/[^/]+/;\n\n/** A pattern with no `:param` segments resolves to exactly one concrete URL, so it can name a\n * nav item. A parameterised one cannot: it stands for a family of URLs, not a menu entry. */\nfunction isConcrete(pattern: string): boolean {\n return !pattern.split(\"/\").some((seg) => seg.startsWith(\":\"));\n}\n\n/**\n * Derive the sidebar entry that owns the current route, as a slug-relative href for\n * `activeNavHref` on a `useHostSidebarActions` input.\n *\n * The source of truth is the plugin's own `routeMeta` — the `{ pattern, label, parent }` chain it\n * already declares for breadcrumbs. That chain IS the plugin's information architecture: it\n * records that `/extensions/projects/people/:id` belongs under `/extensions/projects/settings`,\n * which no amount of URL inspection can recover, because the two share no path. So this walks the\n * matched route up its `parent` chain and returns the nearest CONCRETE (param-free) ancestor:\n *\n * - `/extensions/projects/people/abc` → `settings` (via parent `/extensions/projects/settings`)\n * - `/extensions/tech/technologies/7/edit` → `technologies` (two parents up, past `…/:id`)\n * - `/extensions/projects/all` → `all` (the route is its own nav item)\n *\n * It serves both mounts: a plugin surface is mounted at `/extensions/<slug>` for tenant/admin\n * audiences and at `/super-admin/extensions/<slug>` for super-admin ones, and the base is\n * stripped from whichever the pathname is under. That matters most at the super-admin mount,\n * where the host's URL election has no detail-context path to fall back on at all.\n *\n * Returns `undefined` — meaning \"no opinion, let the host's URL election decide\" — when the path\n * matches no route, when no ancestor is concrete, or when the answer is the plugin ROOT. The root\n * is deliberately not declarable: its href prefixes every URL in the plugin, so the URL election\n * already picks it whenever nothing deeper matches, and there is no case where declaring it\n * changes an outcome.\n *\n * The result is a HINT. The host validates it against the nav items it is actually rendering and\n * falls back to its URL election if it names nothing, so a stale or mistaken `routeMeta` entry\n * degrades to today's behaviour rather than blanking the sidebar.\n */\nexport function resolveActiveNavHref(\n pathname: string,\n routeMeta: RouteMeta,\n opts: ResolveActiveNavHrefOptions = {},\n): string | undefined {\n const base = opts.basePath ?? MOUNT_BASE_PATTERN.exec(pathname)?.[0];\n if (!base) return undefined;\n const mountBase = base.endsWith(\"/\") ? base.slice(0, -1) : base;\n\n const match = matchRoute(pathname, sortBySpecificity(routeMeta));\n if (match == null) return undefined;\n\n // Walk leaf → root, guarding against a cyclic `parent` chain exactly as `buildBreadcrumbs` does.\n const seen = new Set<string>([match.entry.pattern]);\n let node: RouteMetaEntry | undefined = match.entry;\n while (node != null) {\n if (isConcrete(node.pattern)) {\n const owner = node.pattern.replace(/\\/$/, \"\");\n if (owner === mountBase) return undefined; // the plugin root — nothing to declare\n const prefix = `${mountBase}/`;\n return owner.startsWith(prefix) ? owner.slice(prefix.length) : undefined;\n }\n const parentPattern: string | undefined = node.parent;\n if (parentPattern == null || seen.has(parentPattern)) return undefined;\n seen.add(parentPattern);\n node = routeMeta.find((e) => e.pattern === parentPattern);\n }\n return undefined;\n}\n\n/**\n * Attach an active-nav declaration to an existing `useHostSidebarActions` input.\n *\n * The input union is discriminated on `mode`, so a plugin cannot just spread the field on without\n * re-narrowing; this does it once, correctly, for every mode — including `mode:\"none\"`, which is\n * what the input builders return on an ordinary list route and is exactly where the declaration\n * matters most. Passing `undefined` returns the input untouched, so the call site needs no branch.\n *\n * ```ts\n * const input = withActiveNavHref(\n * menuConfigToHostSidebarInput(menuConfig, { pathname, onDispatch }),\n * resolveActiveNavHref(pathname, routeMeta),\n * );\n * useHostSidebarActions(input);\n * ```\n */\nexport function withActiveNavHref(\n input: HostSidebarHookInput,\n activeNavHref: string | undefined,\n): HostSidebarHookInput {\n if (!activeNavHref) return input;\n return { ...input, activeNavHref };\n}\n"]}
@@ -888,10 +888,10 @@ function toContribution(input) {
888
888
  ...activeNav
889
889
  };
890
890
  }
891
- function contributionKey(input) {
891
+ function contributionKey(input, hostToken) {
892
892
  const activeNav = input.activeNavHref ?? "";
893
893
  if (input.mode === SIDEBAR_MODE.none) {
894
- return JSON.stringify([SIDEBAR_MODE.none, activeNav]);
894
+ return JSON.stringify(activeNav ? [SIDEBAR_MODE.none, activeNav, hostToken] : [SIDEBAR_MODE.none, activeNav]);
895
895
  }
896
896
  const actionTuple = (a) => [
897
897
  a.id,
@@ -969,7 +969,13 @@ function useHostSidebarActions(input) {
969
969
  unsub();
970
970
  };
971
971
  }, [compatible, pageId]);
972
- const key = contributionKey(resolved);
972
+ const key = contributionKey(
973
+ resolved,
974
+ deriveHostEntityToken(
975
+ typeof window !== "undefined" ? window.location.pathname : "",
976
+ surfaceBase?.basePath
977
+ )
978
+ );
973
979
  react.useEffect(() => {
974
980
  if (!compatible) return;
975
981
  compatible.publish(toContribution(resolved));
@@ -1144,11 +1150,12 @@ function matchRoute(pathname, sorted) {
1144
1150
  }
1145
1151
 
1146
1152
  // src/platform-react/activeNavHref.ts
1153
+ var MOUNT_BASE_PATTERN = /^(?:\/super-admin)?\/extensions\/[^/]+/;
1147
1154
  function isConcrete(pattern) {
1148
1155
  return !pattern.split("/").some((seg) => seg.startsWith(":"));
1149
1156
  }
1150
1157
  function resolveActiveNavHref(pathname, routeMeta, opts = {}) {
1151
- const base = opts.basePath ?? /^\/extensions\/[^/]+/.exec(pathname)?.[0];
1158
+ const base = opts.basePath ?? MOUNT_BASE_PATTERN.exec(pathname)?.[0];
1152
1159
  if (!base) return void 0;
1153
1160
  const mountBase = base.endsWith("/") ? base.slice(0, -1) : base;
1154
1161
  const match = matchRoute(pathname, sortBySpecificity(routeMeta));