@the-portland-company/shell 0.75.2 → 0.76.1

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.
@@ -188,6 +188,8 @@ type PolitogyFrameData = {
188
188
  devNotes?: ChromeDevNotesConfig;
189
189
  /** Controlled org state for apps that own active-org + switching. */
190
190
  org?: ControlledOrgState;
191
+ /** Auth-owned active org from verified token claims (v2). Wins over `org`. */
192
+ tokenOrg?: TokenOrgState;
191
193
  /**
192
194
  * App-owned children for a standard nav entry this app owns (Contacts).
193
195
  * `count` is the live contact total, pre-formatted ("31.9K"), drawn
@@ -393,6 +395,34 @@ type ControlledOrgState = {
393
395
  */
394
396
  onExternalSwitch?: (id: string) => void;
395
397
  };
398
+ /**
399
+ * Auth-owned active org (Global-Rules v38, UI-Platform v46). The app server
400
+ * passes the VERIFIED token claims; the shell never resolves the org itself.
401
+ */
402
+ type TokenOrgState = {
403
+ /** `org_id` claim from the app's verified token (full uuid). */
404
+ orgId: string | null;
405
+ /** `org_role` claim (raw VRM Core role). */
406
+ orgRole?: string | null;
407
+ /**
408
+ * The switcher list from the app server (names for the pill). When given,
409
+ * the chrome `/api/orgs` call is skipped. When absent it is the fallback.
410
+ */
411
+ orgs?: (NavOrgOption & {
412
+ rawRole?: string | null;
413
+ })[];
414
+ /**
415
+ * Server action that re-exchanges for tokens on `orgId`. Called after the
416
+ * IdP accepted the switch (shared) or for a per-tab override (`perTab`).
417
+ */
418
+ onReexchange: (orgId: string, opts: {
419
+ perTab: boolean;
420
+ }) => void | Promise<void>;
421
+ /** IdP base override (default: auth / auth-staging by host). */
422
+ authBaseUrl?: string;
423
+ /** Called when the IdP refuses a switch (403 not a member, 401 no session). */
424
+ onSwitchError?: (err: unknown) => void;
425
+ };
396
426
  type ChromeDevNotesConfig = {
397
427
  /** Override the DevNotes API base (default: `${proxyBasePath}/devnotes`). */
398
428
  basePath?: string;
@@ -417,7 +447,7 @@ type ChromeDevNotesConfig = {
417
447
  * <ShellFrame …/>
418
448
  * </PolitogyAppChrome>
419
449
  */
420
- declare function PolitogyAppChrome({ storagePrefix, proxyBasePath, fetchJson: fetchJsonOverride, devNotes, org, identity, children, }: {
450
+ declare function PolitogyAppChrome({ storagePrefix, proxyBasePath, fetchJson: fetchJsonOverride, devNotes, org, tokenOrg, identity, children, }: {
421
451
  storagePrefix: string;
422
452
  proxyBasePath?: string;
423
453
  /**
@@ -430,6 +460,8 @@ declare function PolitogyAppChrome({ storagePrefix, proxyBasePath, fetchJson: fe
430
460
  devNotes?: ChromeDevNotesConfig;
431
461
  /** Controlled org state — the app owns active-org + switching (Contacts). */
432
462
  org?: ControlledOrgState;
463
+ /** Auth-owned active org from verified token claims (v2). Wins over `org`. */
464
+ tokenOrg?: TokenOrgState;
433
465
  identity: ChromeIdentity;
434
466
  children: ReactNode;
435
467
  }): react_jsx_runtime.JSX.Element;
@@ -502,4 +534,4 @@ type UseChromeModeArg = ModeId | {
502
534
  */
503
535
  declare function useChromeMode(arg?: UseChromeModeArg): NavMode;
504
536
 
505
- export { type BootPhase, type BootProgress, type BootStageId, type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, LoadingScreen, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppMode, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type PolitogyFrameGrant, type PolitogyGrantedFrame, type ResolvedIdentity, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
537
+ export { type BootPhase, type BootProgress, type BootStageId, type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, LoadingScreen, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppMode, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type PolitogyFrameGrant, type PolitogyGrantedFrame, type ResolvedIdentity, type TokenOrgState, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
@@ -188,6 +188,8 @@ type PolitogyFrameData = {
188
188
  devNotes?: ChromeDevNotesConfig;
189
189
  /** Controlled org state for apps that own active-org + switching. */
190
190
  org?: ControlledOrgState;
191
+ /** Auth-owned active org from verified token claims (v2). Wins over `org`. */
192
+ tokenOrg?: TokenOrgState;
191
193
  /**
192
194
  * App-owned children for a standard nav entry this app owns (Contacts).
193
195
  * `count` is the live contact total, pre-formatted ("31.9K"), drawn
@@ -393,6 +395,34 @@ type ControlledOrgState = {
393
395
  */
394
396
  onExternalSwitch?: (id: string) => void;
395
397
  };
398
+ /**
399
+ * Auth-owned active org (Global-Rules v38, UI-Platform v46). The app server
400
+ * passes the VERIFIED token claims; the shell never resolves the org itself.
401
+ */
402
+ type TokenOrgState = {
403
+ /** `org_id` claim from the app's verified token (full uuid). */
404
+ orgId: string | null;
405
+ /** `org_role` claim (raw VRM Core role). */
406
+ orgRole?: string | null;
407
+ /**
408
+ * The switcher list from the app server (names for the pill). When given,
409
+ * the chrome `/api/orgs` call is skipped. When absent it is the fallback.
410
+ */
411
+ orgs?: (NavOrgOption & {
412
+ rawRole?: string | null;
413
+ })[];
414
+ /**
415
+ * Server action that re-exchanges for tokens on `orgId`. Called after the
416
+ * IdP accepted the switch (shared) or for a per-tab override (`perTab`).
417
+ */
418
+ onReexchange: (orgId: string, opts: {
419
+ perTab: boolean;
420
+ }) => void | Promise<void>;
421
+ /** IdP base override (default: auth / auth-staging by host). */
422
+ authBaseUrl?: string;
423
+ /** Called when the IdP refuses a switch (403 not a member, 401 no session). */
424
+ onSwitchError?: (err: unknown) => void;
425
+ };
396
426
  type ChromeDevNotesConfig = {
397
427
  /** Override the DevNotes API base (default: `${proxyBasePath}/devnotes`). */
398
428
  basePath?: string;
@@ -417,7 +447,7 @@ type ChromeDevNotesConfig = {
417
447
  * <ShellFrame …/>
418
448
  * </PolitogyAppChrome>
419
449
  */
420
- declare function PolitogyAppChrome({ storagePrefix, proxyBasePath, fetchJson: fetchJsonOverride, devNotes, org, identity, children, }: {
450
+ declare function PolitogyAppChrome({ storagePrefix, proxyBasePath, fetchJson: fetchJsonOverride, devNotes, org, tokenOrg, identity, children, }: {
421
451
  storagePrefix: string;
422
452
  proxyBasePath?: string;
423
453
  /**
@@ -430,6 +460,8 @@ declare function PolitogyAppChrome({ storagePrefix, proxyBasePath, fetchJson: fe
430
460
  devNotes?: ChromeDevNotesConfig;
431
461
  /** Controlled org state — the app owns active-org + switching (Contacts). */
432
462
  org?: ControlledOrgState;
463
+ /** Auth-owned active org from verified token claims (v2). Wins over `org`. */
464
+ tokenOrg?: TokenOrgState;
433
465
  identity: ChromeIdentity;
434
466
  children: ReactNode;
435
467
  }): react_jsx_runtime.JSX.Element;
@@ -502,4 +534,4 @@ type UseChromeModeArg = ModeId | {
502
534
  */
503
535
  declare function useChromeMode(arg?: UseChromeModeArg): NavMode;
504
536
 
505
- export { type BootPhase, type BootProgress, type BootStageId, type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, LoadingScreen, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppMode, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type PolitogyFrameGrant, type PolitogyGrantedFrame, type ResolvedIdentity, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
537
+ export { type BootPhase, type BootProgress, type BootStageId, type ChromeDevNotesConfig, type ChromeIdentity, ChromeTopBarActions, type ControlledOrgState, LoadingScreen, POLITOGY_APPS, PolitogyAppChrome, PolitogyAppFrame, type PolitogyAppKey, type PolitogyAppMode, type PolitogyAppSession, type PolitogyBuildMeta, type PolitogyFrameData, type PolitogyFrameGrant, type PolitogyGrantedFrame, type ResolvedIdentity, type TokenOrgState, deriveBreadcrumbs, looksLikeEmail, resolveIdentity, standardNavIcons, useChromeFooter, useChromeIdentity, useChromeMode, useChromeState, useChromeUserMenu, useIsSuperAdmin };
@@ -1,11 +1,11 @@
1
1
  "use client";
2
2
  import { broadcastLogout, watchLogout } from './chunk-LL6LSJAH.js';
3
- import { clearBetaBannerDismissal, standardPolitogyNav, standardNavIcons, isNavItemActive, NavAppShell, NavChromeProvider, TopBarWidgetSlot, FuelAction, SpecOverlayAction, NotificationsAction, SupportAction, useChromeMe, useThemePreference, useNavChromeFetcher, useChromeOrgs } from './chunk-2DXTSTII.js';
4
- export { standardNavIcons } from './chunk-2DXTSTII.js';
3
+ import { clearBetaBannerDismissal, standardPolitogyNav, standardNavIcons, isNavItemActive, NavAppShell, NavChromeProvider, TopBarWidgetSlot, FuelAction, SpecOverlayAction, NotificationsAction, SupportAction, useChromeMe, useThemePreference, useNavChromeDedupedFetch, useChromeOrgs, useNavChromeFetcher } from './chunk-6KX3DWBC.js';
4
+ export { standardNavIcons } from './chunk-6KX3DWBC.js';
5
5
  import { warnDeprecated } from './chunk-MY6G3LMN.js';
6
6
  import './chunk-BHRVZPZ7.js';
7
- import { NAV_DISPLAY_DEFAULT, resolveNavDisplay, isNavDisplayPreference, writeNavDisplayCookie, writeNavDisplayLocal, NAV_DISPLAY_STORAGE_KEY, readNavDisplayCookie } from './chunk-7YVJ7HQG.js';
8
- import { resolveActiveOrg, isOrgId, writeActiveOrgCookie, ACTIVE_ORG_QUERY_PARAM, readActiveOrgCookie } from './chunk-W2HITMV5.js';
7
+ import { NAV_DISPLAY_DEFAULT, resolveNavDisplay, writeNavDisplayLocal, writeNavDisplayCookie, isNavDisplayPreference, NAV_DISPLAY_STORAGE_KEY, readNavDisplayCookie } from './chunk-7YVJ7HQG.js';
8
+ import { readOrgSelectorFromUrl, matchOrgSelector, isOrgId, switchActiveOrg, ACTIVE_ORG_QUERY_PARAM, ORG_ID_QUERY_PARAM, orgIdTail, writeActiveOrgCookie, resolveActiveOrg, readActiveOrgCookie } from './chunk-BWV52AT6.js';
9
9
  import { createContext, useMemo, useRef, useEffect, useState, useCallback, useContext } from 'react';
10
10
  import { DevNotesMenu, createDevNotesClient, DevNotesProvider, DevNotesTaskListModal, DevNotesOverlay } from '@the-portland-company/devnotes';
11
11
  import { LuNotebookPen, LuContactRound, LuMegaphone, LuFilePenLine } from 'react-icons/lu';
@@ -605,6 +605,7 @@ function PolitogyAppFrame({
605
605
  fetchJson: data?.fetchJson,
606
606
  devNotes: data?.devNotes,
607
607
  org: data?.org,
608
+ tokenOrg: data?.tokenOrg,
608
609
  identity: {
609
610
  userId: session.userId,
610
611
  userName: session.userName ?? null,
@@ -885,6 +886,107 @@ var ChromeStateContext = createContext(null);
885
886
  function useChromeState() {
886
887
  return useContext(ChromeStateContext);
887
888
  }
889
+ function TokenChromeStateProvider({
890
+ tokenOrg,
891
+ children
892
+ }) {
893
+ const { orgs: fetchedOrgs, loading: orgsFetching, error: orgsError } = useChromeOrgs({
894
+ enabled: !tokenOrg.orgs
895
+ });
896
+ const [navDisplay, setNavDisplayState] = useState(NAV_DISPLAY_DEFAULT);
897
+ const [pendingOrg, setPendingOrg] = useState(null);
898
+ const tokenOrgRef = useRef(tokenOrg);
899
+ tokenOrgRef.current = tokenOrg;
900
+ useEffect(() => {
901
+ setNavDisplayState(resolveNavDisplay());
902
+ }, []);
903
+ const orgs = useMemo(
904
+ () => tokenOrg.orgs ?? (fetchedOrgs ?? []).map((o) => ({
905
+ id: o.id,
906
+ name: o.name,
907
+ logoUrl: o.logoUrl,
908
+ brandColor: o.brandColor,
909
+ role: o.role,
910
+ rawRole: o.rawRole ?? null
911
+ })),
912
+ [tokenOrg.orgs, fetchedOrgs]
913
+ );
914
+ const [urlSelector] = useState(
915
+ () => typeof window === "undefined" ? null : readOrgSelectorFromUrl(window.location.search)
916
+ );
917
+ const overrideOrg = useMemo(
918
+ () => urlSelector ? matchOrgSelector(urlSelector, orgs)?.id ?? null : null,
919
+ [urlSelector, orgs]
920
+ );
921
+ const orgId = pendingOrg ?? (tokenOrg.orgId && overrideOrg ? overrideOrg : tokenOrg.orgId);
922
+ const reexchangedFor = useRef(null);
923
+ useEffect(() => {
924
+ if (!overrideOrg || !tokenOrg.orgId || overrideOrg === tokenOrg.orgId) return;
925
+ if (reexchangedFor.current === overrideOrg) return;
926
+ reexchangedFor.current = overrideOrg;
927
+ void Promise.resolve(tokenOrgRef.current.onReexchange(overrideOrg, { perTab: true })).catch(
928
+ () => {
929
+ }
930
+ );
931
+ }, [overrideOrg, tokenOrg.orgId]);
932
+ useEffect(() => {
933
+ if (pendingOrg && tokenOrg.orgId === pendingOrg) setPendingOrg(null);
934
+ }, [pendingOrg, tokenOrg.orgId]);
935
+ const setOrgId = useCallback((id) => {
936
+ const current = tokenOrgRef.current;
937
+ if (!isOrgId(id) || id === (current.orgId ?? null)) return;
938
+ setPendingOrg(id);
939
+ void (async () => {
940
+ try {
941
+ const claims = await switchActiveOrg(id, { authBaseUrl: current.authBaseUrl });
942
+ try {
943
+ const url = new URL(window.location.href);
944
+ url.searchParams.delete(ACTIVE_ORG_QUERY_PARAM);
945
+ url.searchParams.set(ORG_ID_QUERY_PARAM, orgIdTail(claims.orgId));
946
+ window.history.replaceState(window.history.state, "", url.toString());
947
+ } catch {
948
+ }
949
+ await tokenOrgRef.current.onReexchange(claims.orgId, { perTab: false });
950
+ } catch (err) {
951
+ setPendingOrg(null);
952
+ tokenOrgRef.current.onSwitchError?.(err);
953
+ }
954
+ })();
955
+ }, []);
956
+ const setNavDisplay = useCallback((m) => {
957
+ setNavDisplayState(m);
958
+ writeNavDisplayLocal(m);
959
+ writeNavDisplayCookie(m);
960
+ }, []);
961
+ useEffect(() => {
962
+ if (!isOrgId(orgId)) return;
963
+ try {
964
+ const url = new URL(window.location.href);
965
+ const tail = orgIdTail(orgId);
966
+ if (url.searchParams.has(ACTIVE_ORG_QUERY_PARAM) || url.searchParams.get(ORG_ID_QUERY_PARAM) !== tail) {
967
+ url.searchParams.delete(ACTIVE_ORG_QUERY_PARAM);
968
+ url.searchParams.set(ORG_ID_QUERY_PARAM, tail);
969
+ window.history.replaceState(window.history.state, "", url.toString());
970
+ }
971
+ } catch {
972
+ }
973
+ }, [orgId]);
974
+ useEffect(() => {
975
+ if (isOrgId(tokenOrg.orgId) && !pendingOrg) writeActiveOrgCookie(tokenOrg.orgId);
976
+ }, [tokenOrg.orgId, pendingOrg]);
977
+ const value = useMemo(
978
+ () => ({
979
+ orgId,
980
+ setOrgId,
981
+ orgs,
982
+ orgsLoading: !tokenOrg.orgs && orgsFetching && !orgsError && !fetchedOrgs,
983
+ navDisplay,
984
+ setNavDisplay
985
+ }),
986
+ [orgId, setOrgId, orgs, tokenOrg.orgs, orgsFetching, orgsError, fetchedOrgs, navDisplay, setNavDisplay]
987
+ );
988
+ return /* @__PURE__ */ jsx(ChromeStateContext.Provider, { value, children });
989
+ }
888
990
  function ChromeStateProvider({
889
991
  controlledOrg,
890
992
  children
@@ -1188,6 +1290,7 @@ function PolitogyAppChrome({
1188
1290
  fetchJson: fetchJsonOverride,
1189
1291
  devNotes,
1190
1292
  org,
1293
+ tokenOrg,
1191
1294
  identity,
1192
1295
  children
1193
1296
  }) {
@@ -1206,7 +1309,7 @@ function PolitogyAppChrome({
1206
1309
  [proxyBasePath]
1207
1310
  );
1208
1311
  const fetchJson = fetchJsonOverride ?? proxyFetchJson;
1209
- return /* @__PURE__ */ jsx(NavChromeProvider, { fetchJson, children: /* @__PURE__ */ jsx(ChromeStateProvider, { controlledOrg: org, children: /* @__PURE__ */ jsx(
1312
+ const devNotesTree = /* @__PURE__ */ jsx(
1210
1313
  AppDevNotes,
1211
1314
  {
1212
1315
  storagePrefix,
@@ -1215,11 +1318,13 @@ function PolitogyAppChrome({
1215
1318
  identity,
1216
1319
  children
1217
1320
  }
1218
- ) }) });
1321
+ );
1322
+ return /* @__PURE__ */ jsx(NavChromeProvider, { fetchJson, children: tokenOrg ? /* @__PURE__ */ jsx(TokenChromeStateProvider, { tokenOrg, children: devNotesTree }) : /* @__PURE__ */ jsx(ChromeStateProvider, { controlledOrg: org, children: devNotesTree }) });
1219
1323
  }
1220
1324
  function ChromeTopBarActions() {
1221
1325
  const state = useChromeState();
1222
1326
  if (!state) return null;
1327
+ const settledOrgId = state.orgsLoading ? null : state.orgId;
1223
1328
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1224
1329
  /* @__PURE__ */ jsx(
1225
1330
  TopBarWidgetSlot,
@@ -1229,10 +1334,10 @@ function ChromeTopBarActions() {
1229
1334
  children: /* @__PURE__ */ jsx(ChromeDevNotesMenu, {})
1230
1335
  }
1231
1336
  ),
1232
- /* @__PURE__ */ jsx(FuelAction, { orgId: state.orgId }),
1337
+ /* @__PURE__ */ jsx(FuelAction, { orgId: settledOrgId }),
1233
1338
  /* @__PURE__ */ jsx(SpecOverlayAction, { orgId: state.orgId, orgName: state.orgs.find((o) => o.id === state.orgId)?.name ?? null }),
1234
- /* @__PURE__ */ jsx(NotificationsAction, { orgId: state.orgId }),
1235
- /* @__PURE__ */ jsx(SupportAction, { orgId: state.orgId })
1339
+ /* @__PURE__ */ jsx(NotificationsAction, { orgId: settledOrgId }),
1340
+ /* @__PURE__ */ jsx(SupportAction, { orgId: state.orgId, orgs: state.orgs.length ? state.orgs : void 0 })
1236
1341
  ] });
1237
1342
  }
1238
1343
  function ChromeDevNotesMenu() {
@@ -1318,7 +1423,8 @@ var MODE_DEFS = [
1318
1423
  function useChromeMode(arg = {}) {
1319
1424
  const { fallbackModeId = "relationship", pinnedModeId } = typeof arg === "string" ? { fallbackModeId: arg, pinnedModeId: void 0 } : arg;
1320
1425
  const ctx = useContext(ChromeStateContext);
1321
- const nav = useNavChromeFetcher();
1426
+ const dedupedFetch = useNavChromeDedupedFetch();
1427
+ const { me, loaded: meLoaded } = useChromeMe();
1322
1428
  const orgId = ctx?.orgId ?? null;
1323
1429
  const [entitled, setEntitled] = useState(null);
1324
1430
  const [selectedModeId, setSelectedModeId] = useState(null);
@@ -1330,9 +1436,14 @@ function useChromeMode(arg = {}) {
1330
1436
  }
1331
1437
  }, []);
1332
1438
  useEffect(() => {
1333
- if (!nav || !orgId) return;
1439
+ if (!dedupedFetch || !orgId || !meLoaded) return;
1440
+ const bootMe = me;
1441
+ if (bootMe && bootMe.account === orgId && bootMe.modes !== void 0) {
1442
+ setEntitled(bootMe.modes ?? null);
1443
+ return;
1444
+ }
1334
1445
  let cancelled = false;
1335
- nav("me", { headers: { "x-contacts-org": orgId } }).then((data) => {
1446
+ dedupedFetch("me", { headers: { "x-contacts-org": orgId } }, { orgId }).then((data) => {
1336
1447
  if (cancelled) return;
1337
1448
  const modes = data?.modes ?? null;
1338
1449
  setEntitled(modes);
@@ -1341,7 +1452,7 @@ function useChromeMode(arg = {}) {
1341
1452
  return () => {
1342
1453
  cancelled = true;
1343
1454
  };
1344
- }, [nav, orgId]);
1455
+ }, [dedupedFetch, me, meLoaded, orgId]);
1345
1456
  const options = MODE_DEFS.filter((m) => entitled?.[m.id]);
1346
1457
  const pinned = pinnedModeId ? MODE_DEFS.find((m) => m.id === pinnedModeId) ?? null : null;
1347
1458
  const selected = selectedModeId ? options.find((m) => m.id === selectedModeId) ?? null : null;