@the-portland-company/shell 0.75.2 → 0.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/active-org.cjs +119 -0
- package/dist/active-org.cjs.map +1 -1
- package/dist/active-org.d.cts +62 -2
- package/dist/active-org.d.ts +62 -2
- package/dist/active-org.js +1 -1
- package/dist/app-chrome.cjs +199 -8
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.d.cts +34 -2
- package/dist/app-chrome.d.ts +34 -2
- package/dist/app-chrome.js +111 -7
- package/dist/app-chrome.js.map +1 -1
- package/dist/chunk-BWV52AT6.js +171 -0
- package/dist/chunk-BWV52AT6.js.map +1 -0
- package/dist/{chunk-2DXTSTII.js → chunk-WHF6YW27.js} +15 -7
- package/dist/chunk-WHF6YW27.js.map +1 -0
- package/dist/native.cjs +13 -5
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +8 -2
- package/dist/native.d.ts +8 -2
- package/dist/native.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-2DXTSTII.js.map +0 -1
- package/dist/chunk-W2HITMV5.js +0 -64
- package/dist/chunk-W2HITMV5.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 0.76.0
|
|
2
|
+
|
|
3
|
+
Auth-owned active org, shell phase (Global-Rules v38, UI-Platform v46; plan `specs/docs/auth-owned-active-org-plan.md` §3.2). Opt-in: no change for apps that do not pass `tokenOrg`.
|
|
4
|
+
|
|
5
|
+
- **`tokenOrg` on `PolitogyAppChrome` / frame data.** The org comes from the app's verified token claims (`org_id`, `org_role`). The pill switch calls `POST {auth}/api/active-org`, then the app's `onReexchange(orgId, { perTab: false })`. A refused switch rolls back and calls `onSwitchError`.
|
|
6
|
+
- **Per-tab override.** `?org_id=` (or legacy `?org=`) scopes this tab only: the chrome calls `onReexchange(orgId, { perTab: true })` and never changes the shared org or the cookie. A non-member selector is ignored.
|
|
7
|
+
- **URL = 7-char tail.** Token mode writes `org_id=<last 7>` and drops `?org=` with a replace. Full uuid and the 6-char tail are accepted.
|
|
8
|
+
- **No per-request chrome `/api/orgs`** when the app passes `tokenOrg.orgs`; `useChromeOrgs({ enabled })` and `SupportAction` reuse the chrome list.
|
|
9
|
+
- **`active-org` helpers:** `switchActiveOrg`, `ActiveOrgSwitchError`, `orgClaimsFromToken`, `orgClaimsFrom`, `orgIdTail`, `isOrgSelector`, `matchOrgSelector`, `readOrgSelectorFromUrl`, `withOrgIdParam`, `authBaseUrl`. `readActiveOrgCookie` / `resolveActiveOrg` are deprecated; the legacy cookie is still written for apps not yet on v2.
|
|
10
|
+
|
|
1
11
|
## 0.75.2
|
|
2
12
|
|
|
3
13
|
Two fixes found on top of 0.75.1:
|
package/dist/active-org.cjs
CHANGED
|
@@ -60,15 +60,134 @@ function withActiveOrgParam(url, orgId) {
|
|
|
60
60
|
return url;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
var ORG_ID_QUERY_PARAM = "org_id";
|
|
64
|
+
var ORG_ID_TAIL_LENGTH = 7;
|
|
65
|
+
var LEGACY_ORG_TAIL_LENGTH = 6;
|
|
66
|
+
var TAIL_RE = /^[0-9a-f]{6,7}$/i;
|
|
67
|
+
var AUTH_BASE_URL = "https://auth.politogyvrm.com";
|
|
68
|
+
var AUTH_BASE_URL_STAGING = "https://auth-staging.politogyvrm.com";
|
|
69
|
+
function orgIdTail(orgId) {
|
|
70
|
+
return isOrgId(orgId) ? orgId.trim().toLowerCase().slice(-ORG_ID_TAIL_LENGTH) : null;
|
|
71
|
+
}
|
|
72
|
+
function isOrgSelector(v) {
|
|
73
|
+
return typeof v === "string" && (UUID_RE.test(v.trim()) || TAIL_RE.test(v.trim()));
|
|
74
|
+
}
|
|
75
|
+
function matchOrgSelector(selector, orgs) {
|
|
76
|
+
if (!isOrgSelector(selector)) return null;
|
|
77
|
+
const sel = selector.trim().toLowerCase();
|
|
78
|
+
if (UUID_RE.test(sel)) return orgs.find((o) => o.id.toLowerCase() === sel) ?? null;
|
|
79
|
+
if (sel.length !== ORG_ID_TAIL_LENGTH && sel.length !== LEGACY_ORG_TAIL_LENGTH) return null;
|
|
80
|
+
const hits = orgs.filter((o) => o.id.toLowerCase().endsWith(sel));
|
|
81
|
+
return hits.length === 1 ? hits[0] : null;
|
|
82
|
+
}
|
|
83
|
+
function readOrgSelectorFromUrl(search) {
|
|
84
|
+
if (!search) return null;
|
|
85
|
+
try {
|
|
86
|
+
const qs = search.includes("?") ? search.slice(search.indexOf("?") + 1) : search;
|
|
87
|
+
const params = new URLSearchParams(qs);
|
|
88
|
+
for (const key of [ORG_ID_QUERY_PARAM, ACTIVE_ORG_QUERY_PARAM]) {
|
|
89
|
+
const v = params.get(key);
|
|
90
|
+
if (isOrgSelector(v)) return v.trim().toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function withOrgIdParam(url, orgId) {
|
|
98
|
+
const tail = orgIdTail(orgId);
|
|
99
|
+
if (!tail) return url;
|
|
100
|
+
try {
|
|
101
|
+
const base = typeof window !== "undefined" ? window.location.origin : "https://politogyvrm.com";
|
|
102
|
+
const u = new URL(url, base);
|
|
103
|
+
u.searchParams.delete(ACTIVE_ORG_QUERY_PARAM);
|
|
104
|
+
u.searchParams.set(ORG_ID_QUERY_PARAM, tail);
|
|
105
|
+
return /^https?:\/\//i.test(url) ? u.toString() : `${u.pathname}${u.search}${u.hash}`;
|
|
106
|
+
} catch {
|
|
107
|
+
return url;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function orgClaimsFrom(claims) {
|
|
111
|
+
if (!claims || typeof claims !== "object") return null;
|
|
112
|
+
const c = claims;
|
|
113
|
+
if (!isOrgId(c.org_id)) return null;
|
|
114
|
+
return {
|
|
115
|
+
orgId: c.org_id.trim().toLowerCase(),
|
|
116
|
+
orgRole: typeof c.org_role === "string" ? c.org_role : null
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function orgClaimsFromToken(token) {
|
|
120
|
+
if (typeof token !== "string") return null;
|
|
121
|
+
const part = token.split(".")[1];
|
|
122
|
+
if (!part) return null;
|
|
123
|
+
try {
|
|
124
|
+
const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
|
|
125
|
+
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
|
|
126
|
+
const json = typeof atob === "function" ? decodeURIComponent(
|
|
127
|
+
Array.from(atob(padded), (ch) => "%" + ch.charCodeAt(0).toString(16).padStart(2, "0")).join("")
|
|
128
|
+
) : "";
|
|
129
|
+
return orgClaimsFrom(JSON.parse(json));
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function authBaseUrl() {
|
|
135
|
+
if (typeof window !== "undefined" && window.location.hostname.toLowerCase().includes("staging")) {
|
|
136
|
+
return AUTH_BASE_URL_STAGING;
|
|
137
|
+
}
|
|
138
|
+
return AUTH_BASE_URL;
|
|
139
|
+
}
|
|
140
|
+
var ActiveOrgSwitchError = class extends Error {
|
|
141
|
+
constructor(status, message) {
|
|
142
|
+
super(message);
|
|
143
|
+
this.status = status;
|
|
144
|
+
this.name = "ActiveOrgSwitchError";
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
async function switchActiveOrg(orgId, opts = {}) {
|
|
148
|
+
if (!isOrgId(orgId)) throw new ActiveOrgSwitchError(400, "org_id must be a full uuid");
|
|
149
|
+
const doFetch = opts.fetch ?? fetch;
|
|
150
|
+
const res = await doFetch(`${opts.authBaseUrl ?? authBaseUrl()}/api/active-org`, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
credentials: "include",
|
|
153
|
+
headers: { "Content-Type": "application/json" },
|
|
154
|
+
body: JSON.stringify({ org_id: orgId })
|
|
155
|
+
});
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
let msg = `active-org switch failed (${res.status})`;
|
|
158
|
+
try {
|
|
159
|
+
const body = await res.json();
|
|
160
|
+
if (body?.error) msg = body.error;
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
throw new ActiveOrgSwitchError(res.status, msg);
|
|
164
|
+
}
|
|
165
|
+
const claims = orgClaimsFrom(await res.json().catch(() => null));
|
|
166
|
+
if (!claims) throw new ActiveOrgSwitchError(502, "active-org switch returned no org_id");
|
|
167
|
+
writeActiveOrgCookie(claims.orgId);
|
|
168
|
+
return claims;
|
|
169
|
+
}
|
|
63
170
|
|
|
64
171
|
exports.ACTIVE_ORG_COOKIE = ACTIVE_ORG_COOKIE;
|
|
65
172
|
exports.ACTIVE_ORG_QUERY_PARAM = ACTIVE_ORG_QUERY_PARAM;
|
|
173
|
+
exports.ActiveOrgSwitchError = ActiveOrgSwitchError;
|
|
174
|
+
exports.ORG_ID_QUERY_PARAM = ORG_ID_QUERY_PARAM;
|
|
175
|
+
exports.ORG_ID_TAIL_LENGTH = ORG_ID_TAIL_LENGTH;
|
|
66
176
|
exports.activeOrgParam = activeOrgParam;
|
|
177
|
+
exports.authBaseUrl = authBaseUrl;
|
|
67
178
|
exports.isOrgId = isOrgId;
|
|
179
|
+
exports.isOrgSelector = isOrgSelector;
|
|
180
|
+
exports.matchOrgSelector = matchOrgSelector;
|
|
181
|
+
exports.orgClaimsFrom = orgClaimsFrom;
|
|
182
|
+
exports.orgClaimsFromToken = orgClaimsFromToken;
|
|
183
|
+
exports.orgIdTail = orgIdTail;
|
|
68
184
|
exports.readActiveOrgCookie = readActiveOrgCookie;
|
|
69
185
|
exports.readActiveOrgFromUrl = readActiveOrgFromUrl;
|
|
186
|
+
exports.readOrgSelectorFromUrl = readOrgSelectorFromUrl;
|
|
70
187
|
exports.resolveActiveOrg = resolveActiveOrg;
|
|
188
|
+
exports.switchActiveOrg = switchActiveOrg;
|
|
71
189
|
exports.withActiveOrgParam = withActiveOrgParam;
|
|
190
|
+
exports.withOrgIdParam = withOrgIdParam;
|
|
72
191
|
exports.writeActiveOrgCookie = writeActiveOrgCookie;
|
|
73
192
|
//# sourceMappingURL=active-org.cjs.map
|
|
74
193
|
//# sourceMappingURL=active-org.cjs.map
|
package/dist/active-org.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/active-org.ts"],"names":[],"mappings":"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","file":"active-org.cjs","sourcesContent":["// Universal active-organization selection — the ONE cross-app contract.\n//\n// A user belongs to many orgs; ONE active-org choice must follow them across\n// every Politogy app + subdomain so all apps resolve the SAME org (and campaign\n// apps don't land on a rel-only org and lock themselves). Layers cooperate,\n// mirroring the theme-preference contract:\n// 1. URL `?org=<uuid>` — the authoritative per-request selector. The shared\n// chrome stamps it onto every route so it is bookmarkable + shareable, and\n// each app forwards it to the chrome API as `x-contacts-org`.\n// 2. `.politogyvrm.com` cookie `politogy_active_org` — cross-subdomain vehicle\n// (auth session cookies are host-only) that remembers the LAST USED org so\n// a bare URL in the next app opens on the same org.\n//\n// The stored/most-authoritative value is the FULL org UUID: contacts-api\n// (auth/vrmMembership.selectRequestedMembership) matches the selector by EXACT\n// id first, then falls back to a short suffix that can COLLIDE across a user's\n// orgs — so the full UUID is the only collision-free selector.\n//\n// Server-safe (no 'use client'): constants/resolve helpers are importable from\n// server layouts; the cookie read/write helpers no-op on the server.\n\nexport const ACTIVE_ORG_COOKIE = 'politogy_active_org'\nexport const ACTIVE_ORG_QUERY_PARAM = 'org'\n\n// Loose UUID shape check — we only ever store ids that came from the user's own\n// authoritative membership list, so this just rejects obvious garbage.\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nexport function isOrgId(v: unknown): v is string {\n return typeof v === 'string' && UUID_RE.test(v.trim())\n}\n\n/** Read the shared active-org cookie (browser only; null on server or unset). */\nexport function readActiveOrgCookie(): string | null {\n if (typeof document === 'undefined') return null\n try {\n const m = document.cookie.match(\n new RegExp(`(?:^|;\\\\s*)${ACTIVE_ORG_COOKIE}=([^;]+)`),\n )\n const v = m ? decodeURIComponent(m[1]) : null\n return isOrgId(v) ? v : null\n } catch {\n return null\n }\n}\n\n// Write the cross-subdomain cookie with the SAME scoping rules as the theme\n// cookie: on a *.politogyvrm.com host scope to the parent domain so every app\n// subdomain reads it; on any other host (localhost, *.pages.dev preview) fall\n// back to a host-only cookie. 1-year, SameSite=Lax, Secure on https.\nexport function writeActiveOrgCookie(orgId: string): void {\n if (typeof document === 'undefined' || typeof window === 'undefined') return\n if (!isOrgId(orgId)) return\n const host = window.location.hostname\n const domainAttr = /(^|\\.)politogyvrm\\.com$/i.test(host) ? '; Domain=.politogyvrm.com' : ''\n const secure = window.location.protocol === 'https:' ? '; Secure' : ''\n document.cookie = `${ACTIVE_ORG_COOKIE}=${encodeURIComponent(\n orgId,\n )}; Path=/; Max-Age=31536000; SameSite=Lax${domainAttr}${secure}`\n}\n\n/**\n * Read the `?org=` selector from a URL search string (full UUID only). Accepts a\n * `location.search`-style string or a full URL.\n */\nexport function readActiveOrgFromUrl(search: string | null | undefined): string | null {\n if (!search) return null\n try {\n const qs = search.includes('?') ? search.slice(search.indexOf('?') + 1) : search\n const v = new URLSearchParams(qs).get(ACTIVE_ORG_QUERY_PARAM)\n return isOrgId(v) ? v : null\n } catch {\n return null\n }\n}\n\n/**\n * Resolve the active org with the cross-app precedence:\n * 1. `?org=<uuid>` in the current URL (explicit per-request selector)\n * 2. the `.politogyvrm.com` cookie (last used, cross-subdomain)\n * 3. null → let the app fall back to the chrome-API default (earliest-joined)\n * Pass `search` on the server (from the request URL); it defaults to\n * `window.location.search` in the browser.\n */\nexport function resolveActiveOrg(search?: string | null): string | null {\n const fromUrl = readActiveOrgFromUrl(\n search ?? (typeof window !== 'undefined' ? window.location.search : null),\n )\n if (fromUrl) return fromUrl\n return readActiveOrgCookie()\n}\n\n/** The canonical `?org=` query fragment for a full org id (no leading `?`/`&`). */\nexport function activeOrgParam(orgId: string | null | undefined): string {\n return isOrgId(orgId) ? `${ACTIVE_ORG_QUERY_PARAM}=${orgId}` : ''\n}\n\n/**\n * Return `url` with `?org=<uuid>` set/replaced. Used by the shared chrome to\n * stamp the active org onto every route so it is always present in the address\n * bar. Leaves the URL unchanged when `orgId` is not a valid id.\n */\nexport function withActiveOrgParam(url: string, orgId: string | null | undefined): string {\n if (!isOrgId(orgId)) return url\n try {\n // Support both absolute and relative URLs.\n const base = typeof window !== 'undefined' ? window.location.origin : 'https://politogyvrm.com'\n const u = new URL(url, base)\n u.searchParams.set(ACTIVE_ORG_QUERY_PARAM, orgId as string)\n // Preserve relative form when the input was relative.\n return /^https?:\\/\\//i.test(url) ? u.toString() : `${u.pathname}${u.search}${u.hash}`\n } catch {\n return url\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/active-org.ts"],"names":[],"mappings":"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","file":"active-org.cjs","sourcesContent":["// Universal active-organization selection — the ONE cross-app contract.\n//\n// A user belongs to many orgs; ONE active-org choice must follow them across\n// every Politogy app + subdomain so all apps resolve the SAME org (and campaign\n// apps don't land on a rel-only org and lock themselves). Layers cooperate,\n// mirroring the theme-preference contract:\n// 1. URL `?org=<uuid>` — the authoritative per-request selector. The shared\n// chrome stamps it onto every route so it is bookmarkable + shareable, and\n// each app forwards it to the chrome API as `x-contacts-org`.\n// 2. `.politogyvrm.com` cookie `politogy_active_org` — cross-subdomain vehicle\n// (auth session cookies are host-only) that remembers the LAST USED org so\n// a bare URL in the next app opens on the same org.\n//\n// The stored/most-authoritative value is the FULL org UUID: contacts-api\n// (auth/vrmMembership.selectRequestedMembership) matches the selector by EXACT\n// id first, then falls back to a short suffix that can COLLIDE across a user's\n// orgs — so the full UUID is the only collision-free selector.\n//\n// Server-safe (no 'use client'): constants/resolve helpers are importable from\n// server layouts; the cookie read/write helpers no-op on the server.\n\nexport const ACTIVE_ORG_COOKIE = 'politogy_active_org'\nexport const ACTIVE_ORG_QUERY_PARAM = 'org'\n\n// Loose UUID shape check — we only ever store ids that came from the user's own\n// authoritative membership list, so this just rejects obvious garbage.\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nexport function isOrgId(v: unknown): v is string {\n return typeof v === 'string' && UUID_RE.test(v.trim())\n}\n\n/**\n * Read the shared active-org cookie (browser only; null on server or unset).\n * @deprecated v2 apps read the org from the token claims (`orgClaimsFromToken`).\n * The cookie stays written during rollout for apps not yet on the token path.\n */\nexport function readActiveOrgCookie(): string | null {\n if (typeof document === 'undefined') return null\n try {\n const m = document.cookie.match(\n new RegExp(`(?:^|;\\\\s*)${ACTIVE_ORG_COOKIE}=([^;]+)`),\n )\n const v = m ? decodeURIComponent(m[1]) : null\n return isOrgId(v) ? v : null\n } catch {\n return null\n }\n}\n\n// Write the cross-subdomain cookie with the SAME scoping rules as the theme\n// cookie: on a *.politogyvrm.com host scope to the parent domain so every app\n// subdomain reads it; on any other host (localhost, *.pages.dev preview) fall\n// back to a host-only cookie. 1-year, SameSite=Lax, Secure on https.\nexport function writeActiveOrgCookie(orgId: string): void {\n if (typeof document === 'undefined' || typeof window === 'undefined') return\n if (!isOrgId(orgId)) return\n const host = window.location.hostname\n const domainAttr = /(^|\\.)politogyvrm\\.com$/i.test(host) ? '; Domain=.politogyvrm.com' : ''\n const secure = window.location.protocol === 'https:' ? '; Secure' : ''\n document.cookie = `${ACTIVE_ORG_COOKIE}=${encodeURIComponent(\n orgId,\n )}; Path=/; Max-Age=31536000; SameSite=Lax${domainAttr}${secure}`\n}\n\n/**\n * Read the `?org=` selector from a URL search string (full UUID only). Accepts a\n * `location.search`-style string or a full URL.\n */\nexport function readActiveOrgFromUrl(search: string | null | undefined): string | null {\n if (!search) return null\n try {\n const qs = search.includes('?') ? search.slice(search.indexOf('?') + 1) : search\n const v = new URLSearchParams(qs).get(ACTIVE_ORG_QUERY_PARAM)\n return isOrgId(v) ? v : null\n } catch {\n return null\n }\n}\n\n/**\n * Resolve the active org with the cross-app precedence:\n * 1. `?org=<uuid>` in the current URL (explicit per-request selector)\n * 2. the `.politogyvrm.com` cookie (last used, cross-subdomain)\n * 3. null → let the app fall back to the chrome-API default (earliest-joined)\n * Pass `search` on the server (from the request URL); it defaults to\n * `window.location.search` in the browser.\n * @deprecated v2 apps use the token `org_id` claim plus `readOrgSelectorFromUrl`\n * as a per-tab override only.\n */\nexport function resolveActiveOrg(search?: string | null): string | null {\n const fromUrl = readActiveOrgFromUrl(\n search ?? (typeof window !== 'undefined' ? window.location.search : null),\n )\n if (fromUrl) return fromUrl\n return readActiveOrgCookie()\n}\n\n/** The canonical `?org=` query fragment for a full org id (no leading `?`/`&`). */\nexport function activeOrgParam(orgId: string | null | undefined): string {\n return isOrgId(orgId) ? `${ACTIVE_ORG_QUERY_PARAM}=${orgId}` : ''\n}\n\n/**\n * Return `url` with `?org=<uuid>` set/replaced. Used by the shared chrome to\n * stamp the active org onto every route so it is always present in the address\n * bar. Leaves the URL unchanged when `orgId` is not a valid id.\n */\nexport function withActiveOrgParam(url: string, orgId: string | null | undefined): string {\n if (!isOrgId(orgId)) return url\n try {\n // Support both absolute and relative URLs.\n const base = typeof window !== 'undefined' ? window.location.origin : 'https://politogyvrm.com'\n const u = new URL(url, base)\n u.searchParams.set(ACTIVE_ORG_QUERY_PARAM, orgId as string)\n // Preserve relative form when the input was relative.\n return /^https?:\\/\\//i.test(url) ? u.toString() : `${u.pathname}${u.search}${u.hash}`\n } catch {\n return url\n }\n}\n\n// ---------------------------------------------------------------------------\n// v2: auth-owned active org (Global-Rules v38 \"Auth / Active Organization\",\n// UI-Platform v46 \"URL state\"). The IdP owns the shared active org on the SSO\n// session; tokens carry `org_id` (full uuid) + `org_role`. The URL carries the\n// 7-char `org_id` tail as a PER-TAB override only: it never changes the shared\n// active org. Full uuids and the legacy 6-char tail are accepted during rollout.\n// ---------------------------------------------------------------------------\n\nexport const ORG_ID_QUERY_PARAM = 'org_id'\nexport const ORG_ID_TAIL_LENGTH = 7\nconst LEGACY_ORG_TAIL_LENGTH = 6\nconst TAIL_RE = /^[0-9a-f]{6,7}$/i\n\nconst AUTH_BASE_URL = 'https://auth.politogyvrm.com'\nconst AUTH_BASE_URL_STAGING = 'https://auth-staging.politogyvrm.com'\n\n/** The URL form of an org id: the last 7 chars of the uuid, lower case. */\nexport function orgIdTail(orgId: string | null | undefined): string | null {\n return isOrgId(orgId) ? (orgId as string).trim().toLowerCase().slice(-ORG_ID_TAIL_LENGTH) : null\n}\n\n/** A full uuid, the 7-char tail, or the legacy 6-char tail. */\nexport function isOrgSelector(v: unknown): v is string {\n return typeof v === 'string' && (UUID_RE.test(v.trim()) || TAIL_RE.test(v.trim()))\n}\n\n/**\n * Resolve a selector against the user's own org list: exact uuid first, then a\n * unique 7-char tail, then a unique 6-char tail. Ambiguous or no match → null\n * (the IdP selector rules, so the shell never picks an org the IdP would not).\n */\nexport function matchOrgSelector<T extends { id: string }>(\n selector: string | null | undefined,\n orgs: readonly T[],\n): T | null {\n if (!isOrgSelector(selector)) return null\n const sel = selector.trim().toLowerCase()\n if (UUID_RE.test(sel)) return orgs.find((o) => o.id.toLowerCase() === sel) ?? null\n if (sel.length !== ORG_ID_TAIL_LENGTH && sel.length !== LEGACY_ORG_TAIL_LENGTH) return null\n const hits = orgs.filter((o) => o.id.toLowerCase().endsWith(sel))\n return hits.length === 1 ? hits[0] : null\n}\n\n/**\n * Read the per-tab org override from a URL: `org_id` (spec), else the legacy\n * `org`. Returns the raw selector (uuid or tail), or null. Never a source for\n * the shared active org.\n */\nexport function readOrgSelectorFromUrl(search: string | null | undefined): string | null {\n if (!search) return null\n try {\n const qs = search.includes('?') ? search.slice(search.indexOf('?') + 1) : search\n const params = new URLSearchParams(qs)\n for (const key of [ORG_ID_QUERY_PARAM, ACTIVE_ORG_QUERY_PARAM]) {\n const v = params.get(key)\n if (isOrgSelector(v)) return v.trim().toLowerCase()\n }\n return null\n } catch {\n return null\n }\n}\n\n/**\n * Return `url` with `org_id=<7-char tail>` set and any legacy `org=` removed.\n * Leaves the URL unchanged when `orgId` is not a full uuid.\n */\nexport function withOrgIdParam(url: string, orgId: string | null | undefined): string {\n const tail = orgIdTail(orgId)\n if (!tail) return url\n try {\n const base = typeof window !== 'undefined' ? window.location.origin : 'https://politogyvrm.com'\n const u = new URL(url, base)\n u.searchParams.delete(ACTIVE_ORG_QUERY_PARAM)\n u.searchParams.set(ORG_ID_QUERY_PARAM, tail)\n return /^https?:\\/\\//i.test(url) ? u.toString() : `${u.pathname}${u.search}${u.hash}`\n } catch {\n return url\n }\n}\n\nexport type OrgClaims = { orgId: string; orgRole: string | null }\n\n/** Pick `org_id` / `org_role` out of a token claim set. */\nexport function orgClaimsFrom(claims: unknown): OrgClaims | null {\n if (!claims || typeof claims !== 'object') return null\n const c = claims as { org_id?: unknown; org_role?: unknown }\n if (!isOrgId(c.org_id)) return null\n return {\n orgId: (c.org_id as string).trim().toLowerCase(),\n orgRole: typeof c.org_role === 'string' ? c.org_role : null,\n }\n}\n\n/**\n * Read `org_id` / `org_role` from a JWT payload WITHOUT verifying it. For\n * display and routing only: the app server verifies the token (JWKS) and every\n * authorization decision keys on the verified claim, never on this.\n */\nexport function orgClaimsFromToken(token: string | null | undefined): OrgClaims | null {\n if (typeof token !== 'string') return null\n const part = token.split('.')[1]\n if (!part) return null\n try {\n const b64 = part.replace(/-/g, '+').replace(/_/g, '/')\n const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4)\n const json =\n typeof atob === 'function'\n ? decodeURIComponent(\n Array.from(atob(padded), (ch) => '%' + ch.charCodeAt(0).toString(16).padStart(2, '0')).join(''),\n )\n : ''\n return orgClaimsFrom(JSON.parse(json))\n } catch {\n return null\n }\n}\n\n/** The IdP base for the current environment (staging host → staging IdP). */\nexport function authBaseUrl(): string {\n if (typeof window !== 'undefined' && window.location.hostname.toLowerCase().includes('staging')) {\n return AUTH_BASE_URL_STAGING\n }\n return AUTH_BASE_URL\n}\n\nexport class ActiveOrgSwitchError extends Error {\n constructor(\n readonly status: number,\n message: string,\n ) {\n super(message)\n this.name = 'ActiveOrgSwitchError'\n }\n}\n\n/**\n * Switch the SHARED active org on the IdP: `POST {auth}/api/active-org`. The\n * IdP checks membership against VRM Core and updates the SSO session; the\n * caller then re-exchanges (server) for tokens carrying the new `org_id`.\n * Also mirrors the legacy cookie so apps not yet on v2 follow the switch.\n * Throws `ActiveOrgSwitchError` (403 = not a member, 401 = no SSO session).\n */\nexport async function switchActiveOrg(\n orgId: string,\n opts: { authBaseUrl?: string; fetch?: typeof fetch } = {},\n): Promise<OrgClaims> {\n if (!isOrgId(orgId)) throw new ActiveOrgSwitchError(400, 'org_id must be a full uuid')\n const doFetch = opts.fetch ?? fetch\n const res = await doFetch(`${opts.authBaseUrl ?? authBaseUrl()}/api/active-org`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ org_id: orgId }),\n })\n if (!res.ok) {\n let msg = `active-org switch failed (${res.status})`\n try {\n const body = (await res.json()) as { error?: string }\n if (body?.error) msg = body.error\n } catch {\n /* no body */\n }\n throw new ActiveOrgSwitchError(res.status, msg)\n }\n const claims = orgClaimsFrom(await res.json().catch(() => null))\n if (!claims) throw new ActiveOrgSwitchError(502, 'active-org switch returned no org_id')\n writeActiveOrgCookie(claims.orgId)\n return claims\n}\n"]}
|
package/dist/active-org.d.cts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
declare const ACTIVE_ORG_COOKIE = "politogy_active_org";
|
|
2
2
|
declare const ACTIVE_ORG_QUERY_PARAM = "org";
|
|
3
3
|
declare function isOrgId(v: unknown): v is string;
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Read the shared active-org cookie (browser only; null on server or unset).
|
|
6
|
+
* @deprecated v2 apps read the org from the token claims (`orgClaimsFromToken`).
|
|
7
|
+
* The cookie stays written during rollout for apps not yet on the token path.
|
|
8
|
+
*/
|
|
5
9
|
declare function readActiveOrgCookie(): string | null;
|
|
6
10
|
declare function writeActiveOrgCookie(orgId: string): void;
|
|
7
11
|
/**
|
|
@@ -16,6 +20,8 @@ declare function readActiveOrgFromUrl(search: string | null | undefined): string
|
|
|
16
20
|
* 3. null → let the app fall back to the chrome-API default (earliest-joined)
|
|
17
21
|
* Pass `search` on the server (from the request URL); it defaults to
|
|
18
22
|
* `window.location.search` in the browser.
|
|
23
|
+
* @deprecated v2 apps use the token `org_id` claim plus `readOrgSelectorFromUrl`
|
|
24
|
+
* as a per-tab override only.
|
|
19
25
|
*/
|
|
20
26
|
declare function resolveActiveOrg(search?: string | null): string | null;
|
|
21
27
|
/** The canonical `?org=` query fragment for a full org id (no leading `?`/`&`). */
|
|
@@ -26,5 +32,59 @@ declare function activeOrgParam(orgId: string | null | undefined): string;
|
|
|
26
32
|
* bar. Leaves the URL unchanged when `orgId` is not a valid id.
|
|
27
33
|
*/
|
|
28
34
|
declare function withActiveOrgParam(url: string, orgId: string | null | undefined): string;
|
|
35
|
+
declare const ORG_ID_QUERY_PARAM = "org_id";
|
|
36
|
+
declare const ORG_ID_TAIL_LENGTH = 7;
|
|
37
|
+
/** The URL form of an org id: the last 7 chars of the uuid, lower case. */
|
|
38
|
+
declare function orgIdTail(orgId: string | null | undefined): string | null;
|
|
39
|
+
/** A full uuid, the 7-char tail, or the legacy 6-char tail. */
|
|
40
|
+
declare function isOrgSelector(v: unknown): v is string;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a selector against the user's own org list: exact uuid first, then a
|
|
43
|
+
* unique 7-char tail, then a unique 6-char tail. Ambiguous or no match → null
|
|
44
|
+
* (the IdP selector rules, so the shell never picks an org the IdP would not).
|
|
45
|
+
*/
|
|
46
|
+
declare function matchOrgSelector<T extends {
|
|
47
|
+
id: string;
|
|
48
|
+
}>(selector: string | null | undefined, orgs: readonly T[]): T | null;
|
|
49
|
+
/**
|
|
50
|
+
* Read the per-tab org override from a URL: `org_id` (spec), else the legacy
|
|
51
|
+
* `org`. Returns the raw selector (uuid or tail), or null. Never a source for
|
|
52
|
+
* the shared active org.
|
|
53
|
+
*/
|
|
54
|
+
declare function readOrgSelectorFromUrl(search: string | null | undefined): string | null;
|
|
55
|
+
/**
|
|
56
|
+
* Return `url` with `org_id=<7-char tail>` set and any legacy `org=` removed.
|
|
57
|
+
* Leaves the URL unchanged when `orgId` is not a full uuid.
|
|
58
|
+
*/
|
|
59
|
+
declare function withOrgIdParam(url: string, orgId: string | null | undefined): string;
|
|
60
|
+
type OrgClaims = {
|
|
61
|
+
orgId: string;
|
|
62
|
+
orgRole: string | null;
|
|
63
|
+
};
|
|
64
|
+
/** Pick `org_id` / `org_role` out of a token claim set. */
|
|
65
|
+
declare function orgClaimsFrom(claims: unknown): OrgClaims | null;
|
|
66
|
+
/**
|
|
67
|
+
* Read `org_id` / `org_role` from a JWT payload WITHOUT verifying it. For
|
|
68
|
+
* display and routing only: the app server verifies the token (JWKS) and every
|
|
69
|
+
* authorization decision keys on the verified claim, never on this.
|
|
70
|
+
*/
|
|
71
|
+
declare function orgClaimsFromToken(token: string | null | undefined): OrgClaims | null;
|
|
72
|
+
/** The IdP base for the current environment (staging host → staging IdP). */
|
|
73
|
+
declare function authBaseUrl(): string;
|
|
74
|
+
declare class ActiveOrgSwitchError extends Error {
|
|
75
|
+
readonly status: number;
|
|
76
|
+
constructor(status: number, message: string);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Switch the SHARED active org on the IdP: `POST {auth}/api/active-org`. The
|
|
80
|
+
* IdP checks membership against VRM Core and updates the SSO session; the
|
|
81
|
+
* caller then re-exchanges (server) for tokens carrying the new `org_id`.
|
|
82
|
+
* Also mirrors the legacy cookie so apps not yet on v2 follow the switch.
|
|
83
|
+
* Throws `ActiveOrgSwitchError` (403 = not a member, 401 = no SSO session).
|
|
84
|
+
*/
|
|
85
|
+
declare function switchActiveOrg(orgId: string, opts?: {
|
|
86
|
+
authBaseUrl?: string;
|
|
87
|
+
fetch?: typeof fetch;
|
|
88
|
+
}): Promise<OrgClaims>;
|
|
29
89
|
|
|
30
|
-
export { ACTIVE_ORG_COOKIE, ACTIVE_ORG_QUERY_PARAM, activeOrgParam, isOrgId, readActiveOrgCookie, readActiveOrgFromUrl, resolveActiveOrg, withActiveOrgParam, writeActiveOrgCookie };
|
|
90
|
+
export { ACTIVE_ORG_COOKIE, ACTIVE_ORG_QUERY_PARAM, ActiveOrgSwitchError, ORG_ID_QUERY_PARAM, ORG_ID_TAIL_LENGTH, type OrgClaims, activeOrgParam, authBaseUrl, isOrgId, isOrgSelector, matchOrgSelector, orgClaimsFrom, orgClaimsFromToken, orgIdTail, readActiveOrgCookie, readActiveOrgFromUrl, readOrgSelectorFromUrl, resolveActiveOrg, switchActiveOrg, withActiveOrgParam, withOrgIdParam, writeActiveOrgCookie };
|
package/dist/active-org.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
declare const ACTIVE_ORG_COOKIE = "politogy_active_org";
|
|
2
2
|
declare const ACTIVE_ORG_QUERY_PARAM = "org";
|
|
3
3
|
declare function isOrgId(v: unknown): v is string;
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Read the shared active-org cookie (browser only; null on server or unset).
|
|
6
|
+
* @deprecated v2 apps read the org from the token claims (`orgClaimsFromToken`).
|
|
7
|
+
* The cookie stays written during rollout for apps not yet on the token path.
|
|
8
|
+
*/
|
|
5
9
|
declare function readActiveOrgCookie(): string | null;
|
|
6
10
|
declare function writeActiveOrgCookie(orgId: string): void;
|
|
7
11
|
/**
|
|
@@ -16,6 +20,8 @@ declare function readActiveOrgFromUrl(search: string | null | undefined): string
|
|
|
16
20
|
* 3. null → let the app fall back to the chrome-API default (earliest-joined)
|
|
17
21
|
* Pass `search` on the server (from the request URL); it defaults to
|
|
18
22
|
* `window.location.search` in the browser.
|
|
23
|
+
* @deprecated v2 apps use the token `org_id` claim plus `readOrgSelectorFromUrl`
|
|
24
|
+
* as a per-tab override only.
|
|
19
25
|
*/
|
|
20
26
|
declare function resolveActiveOrg(search?: string | null): string | null;
|
|
21
27
|
/** The canonical `?org=` query fragment for a full org id (no leading `?`/`&`). */
|
|
@@ -26,5 +32,59 @@ declare function activeOrgParam(orgId: string | null | undefined): string;
|
|
|
26
32
|
* bar. Leaves the URL unchanged when `orgId` is not a valid id.
|
|
27
33
|
*/
|
|
28
34
|
declare function withActiveOrgParam(url: string, orgId: string | null | undefined): string;
|
|
35
|
+
declare const ORG_ID_QUERY_PARAM = "org_id";
|
|
36
|
+
declare const ORG_ID_TAIL_LENGTH = 7;
|
|
37
|
+
/** The URL form of an org id: the last 7 chars of the uuid, lower case. */
|
|
38
|
+
declare function orgIdTail(orgId: string | null | undefined): string | null;
|
|
39
|
+
/** A full uuid, the 7-char tail, or the legacy 6-char tail. */
|
|
40
|
+
declare function isOrgSelector(v: unknown): v is string;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a selector against the user's own org list: exact uuid first, then a
|
|
43
|
+
* unique 7-char tail, then a unique 6-char tail. Ambiguous or no match → null
|
|
44
|
+
* (the IdP selector rules, so the shell never picks an org the IdP would not).
|
|
45
|
+
*/
|
|
46
|
+
declare function matchOrgSelector<T extends {
|
|
47
|
+
id: string;
|
|
48
|
+
}>(selector: string | null | undefined, orgs: readonly T[]): T | null;
|
|
49
|
+
/**
|
|
50
|
+
* Read the per-tab org override from a URL: `org_id` (spec), else the legacy
|
|
51
|
+
* `org`. Returns the raw selector (uuid or tail), or null. Never a source for
|
|
52
|
+
* the shared active org.
|
|
53
|
+
*/
|
|
54
|
+
declare function readOrgSelectorFromUrl(search: string | null | undefined): string | null;
|
|
55
|
+
/**
|
|
56
|
+
* Return `url` with `org_id=<7-char tail>` set and any legacy `org=` removed.
|
|
57
|
+
* Leaves the URL unchanged when `orgId` is not a full uuid.
|
|
58
|
+
*/
|
|
59
|
+
declare function withOrgIdParam(url: string, orgId: string | null | undefined): string;
|
|
60
|
+
type OrgClaims = {
|
|
61
|
+
orgId: string;
|
|
62
|
+
orgRole: string | null;
|
|
63
|
+
};
|
|
64
|
+
/** Pick `org_id` / `org_role` out of a token claim set. */
|
|
65
|
+
declare function orgClaimsFrom(claims: unknown): OrgClaims | null;
|
|
66
|
+
/**
|
|
67
|
+
* Read `org_id` / `org_role` from a JWT payload WITHOUT verifying it. For
|
|
68
|
+
* display and routing only: the app server verifies the token (JWKS) and every
|
|
69
|
+
* authorization decision keys on the verified claim, never on this.
|
|
70
|
+
*/
|
|
71
|
+
declare function orgClaimsFromToken(token: string | null | undefined): OrgClaims | null;
|
|
72
|
+
/** The IdP base for the current environment (staging host → staging IdP). */
|
|
73
|
+
declare function authBaseUrl(): string;
|
|
74
|
+
declare class ActiveOrgSwitchError extends Error {
|
|
75
|
+
readonly status: number;
|
|
76
|
+
constructor(status: number, message: string);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Switch the SHARED active org on the IdP: `POST {auth}/api/active-org`. The
|
|
80
|
+
* IdP checks membership against VRM Core and updates the SSO session; the
|
|
81
|
+
* caller then re-exchanges (server) for tokens carrying the new `org_id`.
|
|
82
|
+
* Also mirrors the legacy cookie so apps not yet on v2 follow the switch.
|
|
83
|
+
* Throws `ActiveOrgSwitchError` (403 = not a member, 401 = no SSO session).
|
|
84
|
+
*/
|
|
85
|
+
declare function switchActiveOrg(orgId: string, opts?: {
|
|
86
|
+
authBaseUrl?: string;
|
|
87
|
+
fetch?: typeof fetch;
|
|
88
|
+
}): Promise<OrgClaims>;
|
|
29
89
|
|
|
30
|
-
export { ACTIVE_ORG_COOKIE, ACTIVE_ORG_QUERY_PARAM, activeOrgParam, isOrgId, readActiveOrgCookie, readActiveOrgFromUrl, resolveActiveOrg, withActiveOrgParam, writeActiveOrgCookie };
|
|
90
|
+
export { ACTIVE_ORG_COOKIE, ACTIVE_ORG_QUERY_PARAM, ActiveOrgSwitchError, ORG_ID_QUERY_PARAM, ORG_ID_TAIL_LENGTH, type OrgClaims, activeOrgParam, authBaseUrl, isOrgId, isOrgSelector, matchOrgSelector, orgClaimsFrom, orgClaimsFromToken, orgIdTail, readActiveOrgCookie, readActiveOrgFromUrl, readOrgSelectorFromUrl, resolveActiveOrg, switchActiveOrg, withActiveOrgParam, withOrgIdParam, writeActiveOrgCookie };
|
package/dist/active-org.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { ACTIVE_ORG_COOKIE, ACTIVE_ORG_QUERY_PARAM, activeOrgParam, isOrgId, readActiveOrgCookie, readActiveOrgFromUrl, resolveActiveOrg, withActiveOrgParam, writeActiveOrgCookie } from './chunk-
|
|
1
|
+
export { ACTIVE_ORG_COOKIE, ACTIVE_ORG_QUERY_PARAM, ActiveOrgSwitchError, ORG_ID_QUERY_PARAM, ORG_ID_TAIL_LENGTH, activeOrgParam, authBaseUrl, isOrgId, isOrgSelector, matchOrgSelector, orgClaimsFrom, orgClaimsFromToken, orgIdTail, readActiveOrgCookie, readActiveOrgFromUrl, readOrgSelectorFromUrl, resolveActiveOrg, switchActiveOrg, withActiveOrgParam, withOrgIdParam, writeActiveOrgCookie } from './chunk-BWV52AT6.js';
|
|
2
2
|
//# sourceMappingURL=active-org.js.map
|
|
3
3
|
//# sourceMappingURL=active-org.js.map
|
package/dist/app-chrome.cjs
CHANGED
|
@@ -550,12 +550,16 @@ function useNavChrome(component) {
|
|
|
550
550
|
}
|
|
551
551
|
return ctx;
|
|
552
552
|
}
|
|
553
|
-
function useChromeOrgs() {
|
|
553
|
+
function useChromeOrgs({ enabled = true } = {}) {
|
|
554
554
|
const { dedupedFetch } = useNavChrome("useChromeOrgs");
|
|
555
555
|
const [orgs, setOrgs] = react.useState(null);
|
|
556
|
-
const [loading, setLoading] = react.useState(
|
|
556
|
+
const [loading, setLoading] = react.useState(enabled);
|
|
557
557
|
const [error, setError] = react.useState(false);
|
|
558
558
|
react.useEffect(() => {
|
|
559
|
+
if (!enabled) {
|
|
560
|
+
setLoading(false);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
559
563
|
let cancelled = false;
|
|
560
564
|
setLoading(true);
|
|
561
565
|
setError(false);
|
|
@@ -571,7 +575,7 @@ function useChromeOrgs() {
|
|
|
571
575
|
return () => {
|
|
572
576
|
cancelled = true;
|
|
573
577
|
};
|
|
574
|
-
}, [dedupedFetch]);
|
|
578
|
+
}, [dedupedFetch, enabled]);
|
|
575
579
|
return { orgs, loading, error };
|
|
576
580
|
}
|
|
577
581
|
function ValueSkeleton({ className }) {
|
|
@@ -1335,10 +1339,14 @@ function composeSupportMessage(input) {
|
|
|
1335
1339
|
---
|
|
1336
1340
|
${ctx.join("\n")}`;
|
|
1337
1341
|
}
|
|
1338
|
-
function SupportAction({
|
|
1342
|
+
function SupportAction({
|
|
1343
|
+
orgId,
|
|
1344
|
+
orgs: knownOrgs
|
|
1345
|
+
} = {}) {
|
|
1339
1346
|
const { fetchJson } = useNavChrome("SupportAction");
|
|
1340
1347
|
const { me, loaded } = useChromeMe();
|
|
1341
|
-
const { orgs } = useChromeOrgs();
|
|
1348
|
+
const { orgs: fetchedOrgs } = useChromeOrgs({ enabled: !knownOrgs });
|
|
1349
|
+
const orgs = knownOrgs ?? fetchedOrgs;
|
|
1342
1350
|
const [open, setOpen] = react.useState(false);
|
|
1343
1351
|
const [message, setMessage] = react.useState("");
|
|
1344
1352
|
const [email, setEmail] = react.useState("");
|
|
@@ -3855,6 +3863,85 @@ function resolveActiveOrg(search) {
|
|
|
3855
3863
|
if (fromUrl) return fromUrl;
|
|
3856
3864
|
return readActiveOrgCookie();
|
|
3857
3865
|
}
|
|
3866
|
+
var ORG_ID_QUERY_PARAM = "org_id";
|
|
3867
|
+
var ORG_ID_TAIL_LENGTH = 7;
|
|
3868
|
+
var LEGACY_ORG_TAIL_LENGTH = 6;
|
|
3869
|
+
var TAIL_RE = /^[0-9a-f]{6,7}$/i;
|
|
3870
|
+
var AUTH_BASE_URL = "https://auth.politogyvrm.com";
|
|
3871
|
+
var AUTH_BASE_URL_STAGING = "https://auth-staging.politogyvrm.com";
|
|
3872
|
+
function orgIdTail(orgId) {
|
|
3873
|
+
return isOrgId(orgId) ? orgId.trim().toLowerCase().slice(-ORG_ID_TAIL_LENGTH) : null;
|
|
3874
|
+
}
|
|
3875
|
+
function isOrgSelector(v) {
|
|
3876
|
+
return typeof v === "string" && (UUID_RE.test(v.trim()) || TAIL_RE.test(v.trim()));
|
|
3877
|
+
}
|
|
3878
|
+
function matchOrgSelector(selector, orgs) {
|
|
3879
|
+
if (!isOrgSelector(selector)) return null;
|
|
3880
|
+
const sel = selector.trim().toLowerCase();
|
|
3881
|
+
if (UUID_RE.test(sel)) return orgs.find((o) => o.id.toLowerCase() === sel) ?? null;
|
|
3882
|
+
if (sel.length !== ORG_ID_TAIL_LENGTH && sel.length !== LEGACY_ORG_TAIL_LENGTH) return null;
|
|
3883
|
+
const hits = orgs.filter((o) => o.id.toLowerCase().endsWith(sel));
|
|
3884
|
+
return hits.length === 1 ? hits[0] : null;
|
|
3885
|
+
}
|
|
3886
|
+
function readOrgSelectorFromUrl(search) {
|
|
3887
|
+
if (!search) return null;
|
|
3888
|
+
try {
|
|
3889
|
+
const qs = search.includes("?") ? search.slice(search.indexOf("?") + 1) : search;
|
|
3890
|
+
const params = new URLSearchParams(qs);
|
|
3891
|
+
for (const key of [ORG_ID_QUERY_PARAM, ACTIVE_ORG_QUERY_PARAM]) {
|
|
3892
|
+
const v = params.get(key);
|
|
3893
|
+
if (isOrgSelector(v)) return v.trim().toLowerCase();
|
|
3894
|
+
}
|
|
3895
|
+
return null;
|
|
3896
|
+
} catch {
|
|
3897
|
+
return null;
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
function orgClaimsFrom(claims) {
|
|
3901
|
+
if (!claims || typeof claims !== "object") return null;
|
|
3902
|
+
const c = claims;
|
|
3903
|
+
if (!isOrgId(c.org_id)) return null;
|
|
3904
|
+
return {
|
|
3905
|
+
orgId: c.org_id.trim().toLowerCase(),
|
|
3906
|
+
orgRole: typeof c.org_role === "string" ? c.org_role : null
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
function authBaseUrl() {
|
|
3910
|
+
if (typeof window !== "undefined" && window.location.hostname.toLowerCase().includes("staging")) {
|
|
3911
|
+
return AUTH_BASE_URL_STAGING;
|
|
3912
|
+
}
|
|
3913
|
+
return AUTH_BASE_URL;
|
|
3914
|
+
}
|
|
3915
|
+
var ActiveOrgSwitchError = class extends Error {
|
|
3916
|
+
constructor(status, message) {
|
|
3917
|
+
super(message);
|
|
3918
|
+
this.status = status;
|
|
3919
|
+
this.name = "ActiveOrgSwitchError";
|
|
3920
|
+
}
|
|
3921
|
+
};
|
|
3922
|
+
async function switchActiveOrg(orgId, opts = {}) {
|
|
3923
|
+
if (!isOrgId(orgId)) throw new ActiveOrgSwitchError(400, "org_id must be a full uuid");
|
|
3924
|
+
const doFetch = opts.fetch ?? fetch;
|
|
3925
|
+
const res = await doFetch(`${opts.authBaseUrl ?? authBaseUrl()}/api/active-org`, {
|
|
3926
|
+
method: "POST",
|
|
3927
|
+
credentials: "include",
|
|
3928
|
+
headers: { "Content-Type": "application/json" },
|
|
3929
|
+
body: JSON.stringify({ org_id: orgId })
|
|
3930
|
+
});
|
|
3931
|
+
if (!res.ok) {
|
|
3932
|
+
let msg = `active-org switch failed (${res.status})`;
|
|
3933
|
+
try {
|
|
3934
|
+
const body = await res.json();
|
|
3935
|
+
if (body?.error) msg = body.error;
|
|
3936
|
+
} catch {
|
|
3937
|
+
}
|
|
3938
|
+
throw new ActiveOrgSwitchError(res.status, msg);
|
|
3939
|
+
}
|
|
3940
|
+
const claims = orgClaimsFrom(await res.json().catch(() => null));
|
|
3941
|
+
if (!claims) throw new ActiveOrgSwitchError(502, "active-org switch returned no org_id");
|
|
3942
|
+
writeActiveOrgCookie(claims.orgId);
|
|
3943
|
+
return claims;
|
|
3944
|
+
}
|
|
3858
3945
|
|
|
3859
3946
|
// src/lib/breadcrumbs.ts
|
|
3860
3947
|
function deriveBreadcrumbs(items, pathname, opts) {
|
|
@@ -4533,6 +4620,7 @@ function PolitogyAppFrame({
|
|
|
4533
4620
|
fetchJson: data?.fetchJson,
|
|
4534
4621
|
devNotes: data?.devNotes,
|
|
4535
4622
|
org: data?.org,
|
|
4623
|
+
tokenOrg: data?.tokenOrg,
|
|
4536
4624
|
identity: {
|
|
4537
4625
|
userId: session.userId,
|
|
4538
4626
|
userName: session.userName ?? null,
|
|
@@ -4813,6 +4901,107 @@ var ChromeStateContext = react.createContext(null);
|
|
|
4813
4901
|
function useChromeState() {
|
|
4814
4902
|
return react.useContext(ChromeStateContext);
|
|
4815
4903
|
}
|
|
4904
|
+
function TokenChromeStateProvider({
|
|
4905
|
+
tokenOrg,
|
|
4906
|
+
children
|
|
4907
|
+
}) {
|
|
4908
|
+
const { orgs: fetchedOrgs, loading: orgsFetching, error: orgsError } = useChromeOrgs({
|
|
4909
|
+
enabled: !tokenOrg.orgs
|
|
4910
|
+
});
|
|
4911
|
+
const [navDisplay, setNavDisplayState] = react.useState(NAV_DISPLAY_DEFAULT);
|
|
4912
|
+
const [pendingOrg, setPendingOrg] = react.useState(null);
|
|
4913
|
+
const tokenOrgRef = react.useRef(tokenOrg);
|
|
4914
|
+
tokenOrgRef.current = tokenOrg;
|
|
4915
|
+
react.useEffect(() => {
|
|
4916
|
+
setNavDisplayState(resolveNavDisplay());
|
|
4917
|
+
}, []);
|
|
4918
|
+
const orgs = react.useMemo(
|
|
4919
|
+
() => tokenOrg.orgs ?? (fetchedOrgs ?? []).map((o) => ({
|
|
4920
|
+
id: o.id,
|
|
4921
|
+
name: o.name,
|
|
4922
|
+
logoUrl: o.logoUrl,
|
|
4923
|
+
brandColor: o.brandColor,
|
|
4924
|
+
role: o.role,
|
|
4925
|
+
rawRole: o.rawRole ?? null
|
|
4926
|
+
})),
|
|
4927
|
+
[tokenOrg.orgs, fetchedOrgs]
|
|
4928
|
+
);
|
|
4929
|
+
const [urlSelector] = react.useState(
|
|
4930
|
+
() => typeof window === "undefined" ? null : readOrgSelectorFromUrl(window.location.search)
|
|
4931
|
+
);
|
|
4932
|
+
const overrideOrg = react.useMemo(
|
|
4933
|
+
() => urlSelector ? matchOrgSelector(urlSelector, orgs)?.id ?? null : null,
|
|
4934
|
+
[urlSelector, orgs]
|
|
4935
|
+
);
|
|
4936
|
+
const orgId = pendingOrg ?? (tokenOrg.orgId && overrideOrg ? overrideOrg : tokenOrg.orgId);
|
|
4937
|
+
const reexchangedFor = react.useRef(null);
|
|
4938
|
+
react.useEffect(() => {
|
|
4939
|
+
if (!overrideOrg || !tokenOrg.orgId || overrideOrg === tokenOrg.orgId) return;
|
|
4940
|
+
if (reexchangedFor.current === overrideOrg) return;
|
|
4941
|
+
reexchangedFor.current = overrideOrg;
|
|
4942
|
+
void Promise.resolve(tokenOrgRef.current.onReexchange(overrideOrg, { perTab: true })).catch(
|
|
4943
|
+
() => {
|
|
4944
|
+
}
|
|
4945
|
+
);
|
|
4946
|
+
}, [overrideOrg, tokenOrg.orgId]);
|
|
4947
|
+
react.useEffect(() => {
|
|
4948
|
+
if (pendingOrg && tokenOrg.orgId === pendingOrg) setPendingOrg(null);
|
|
4949
|
+
}, [pendingOrg, tokenOrg.orgId]);
|
|
4950
|
+
const setOrgId = react.useCallback((id) => {
|
|
4951
|
+
const current = tokenOrgRef.current;
|
|
4952
|
+
if (!isOrgId(id) || id === (current.orgId ?? null)) return;
|
|
4953
|
+
setPendingOrg(id);
|
|
4954
|
+
void (async () => {
|
|
4955
|
+
try {
|
|
4956
|
+
const claims = await switchActiveOrg(id, { authBaseUrl: current.authBaseUrl });
|
|
4957
|
+
try {
|
|
4958
|
+
const url = new URL(window.location.href);
|
|
4959
|
+
url.searchParams.delete(ACTIVE_ORG_QUERY_PARAM);
|
|
4960
|
+
url.searchParams.set(ORG_ID_QUERY_PARAM, orgIdTail(claims.orgId));
|
|
4961
|
+
window.history.replaceState(window.history.state, "", url.toString());
|
|
4962
|
+
} catch {
|
|
4963
|
+
}
|
|
4964
|
+
await tokenOrgRef.current.onReexchange(claims.orgId, { perTab: false });
|
|
4965
|
+
} catch (err) {
|
|
4966
|
+
setPendingOrg(null);
|
|
4967
|
+
tokenOrgRef.current.onSwitchError?.(err);
|
|
4968
|
+
}
|
|
4969
|
+
})();
|
|
4970
|
+
}, []);
|
|
4971
|
+
const setNavDisplay = react.useCallback((m) => {
|
|
4972
|
+
setNavDisplayState(m);
|
|
4973
|
+
writeNavDisplayLocal(m);
|
|
4974
|
+
writeNavDisplayCookie(m);
|
|
4975
|
+
}, []);
|
|
4976
|
+
react.useEffect(() => {
|
|
4977
|
+
if (!isOrgId(orgId)) return;
|
|
4978
|
+
try {
|
|
4979
|
+
const url = new URL(window.location.href);
|
|
4980
|
+
const tail = orgIdTail(orgId);
|
|
4981
|
+
if (url.searchParams.has(ACTIVE_ORG_QUERY_PARAM) || url.searchParams.get(ORG_ID_QUERY_PARAM) !== tail) {
|
|
4982
|
+
url.searchParams.delete(ACTIVE_ORG_QUERY_PARAM);
|
|
4983
|
+
url.searchParams.set(ORG_ID_QUERY_PARAM, tail);
|
|
4984
|
+
window.history.replaceState(window.history.state, "", url.toString());
|
|
4985
|
+
}
|
|
4986
|
+
} catch {
|
|
4987
|
+
}
|
|
4988
|
+
}, [orgId]);
|
|
4989
|
+
react.useEffect(() => {
|
|
4990
|
+
if (isOrgId(tokenOrg.orgId) && !pendingOrg) writeActiveOrgCookie(tokenOrg.orgId);
|
|
4991
|
+
}, [tokenOrg.orgId, pendingOrg]);
|
|
4992
|
+
const value = react.useMemo(
|
|
4993
|
+
() => ({
|
|
4994
|
+
orgId,
|
|
4995
|
+
setOrgId,
|
|
4996
|
+
orgs,
|
|
4997
|
+
orgsLoading: !tokenOrg.orgs && orgsFetching && !orgsError && !fetchedOrgs,
|
|
4998
|
+
navDisplay,
|
|
4999
|
+
setNavDisplay
|
|
5000
|
+
}),
|
|
5001
|
+
[orgId, setOrgId, orgs, tokenOrg.orgs, orgsFetching, orgsError, fetchedOrgs, navDisplay, setNavDisplay]
|
|
5002
|
+
);
|
|
5003
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ChromeStateContext.Provider, { value, children });
|
|
5004
|
+
}
|
|
4816
5005
|
function ChromeStateProvider({
|
|
4817
5006
|
controlledOrg,
|
|
4818
5007
|
children
|
|
@@ -5116,6 +5305,7 @@ function PolitogyAppChrome({
|
|
|
5116
5305
|
fetchJson: fetchJsonOverride,
|
|
5117
5306
|
devNotes,
|
|
5118
5307
|
org,
|
|
5308
|
+
tokenOrg,
|
|
5119
5309
|
identity,
|
|
5120
5310
|
children
|
|
5121
5311
|
}) {
|
|
@@ -5134,7 +5324,7 @@ function PolitogyAppChrome({
|
|
|
5134
5324
|
[proxyBasePath]
|
|
5135
5325
|
);
|
|
5136
5326
|
const fetchJson = fetchJsonOverride ?? proxyFetchJson;
|
|
5137
|
-
|
|
5327
|
+
const devNotesTree = /* @__PURE__ */ jsxRuntime.jsx(
|
|
5138
5328
|
AppDevNotes,
|
|
5139
5329
|
{
|
|
5140
5330
|
storagePrefix,
|
|
@@ -5143,7 +5333,8 @@ function PolitogyAppChrome({
|
|
|
5143
5333
|
identity,
|
|
5144
5334
|
children
|
|
5145
5335
|
}
|
|
5146
|
-
)
|
|
5336
|
+
);
|
|
5337
|
+
return /* @__PURE__ */ jsxRuntime.jsx(NavChromeProvider, { fetchJson, children: tokenOrg ? /* @__PURE__ */ jsxRuntime.jsx(TokenChromeStateProvider, { tokenOrg, children: devNotesTree }) : /* @__PURE__ */ jsxRuntime.jsx(ChromeStateProvider, { controlledOrg: org, children: devNotesTree }) });
|
|
5147
5338
|
}
|
|
5148
5339
|
function ChromeTopBarActions() {
|
|
5149
5340
|
const state = useChromeState();
|
|
@@ -5160,7 +5351,7 @@ function ChromeTopBarActions() {
|
|
|
5160
5351
|
/* @__PURE__ */ jsxRuntime.jsx(FuelAction, { orgId: state.orgId }),
|
|
5161
5352
|
/* @__PURE__ */ jsxRuntime.jsx(SpecOverlayAction, { orgId: state.orgId, orgName: state.orgs.find((o) => o.id === state.orgId)?.name ?? null }),
|
|
5162
5353
|
/* @__PURE__ */ jsxRuntime.jsx(NotificationsAction, { orgId: state.orgId }),
|
|
5163
|
-
/* @__PURE__ */ jsxRuntime.jsx(SupportAction, { orgId: state.orgId })
|
|
5354
|
+
/* @__PURE__ */ jsxRuntime.jsx(SupportAction, { orgId: state.orgId, orgs: state.orgs.length ? state.orgs : void 0 })
|
|
5164
5355
|
] });
|
|
5165
5356
|
}
|
|
5166
5357
|
function ChromeDevNotesMenu() {
|