@burtson-labs/bandit-nav 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,10 +7,23 @@ settings live; the package decides which items are local routes and which
7
7
  cross to the other site. Cross-site items are ordinary same-tab links, and
8
8
  single sign-on between the two sites makes the destination open signed in.
9
9
 
10
+ Agents and Artifacts are Stealth, and Stealth access is membership in a
11
+ Stealth organization: a user outside every organization sees **Chat ·
12
+ Settings** only, so the bar never offers a link that dead-ends.
13
+
10
14
  ```tsx
11
- import { ProductNav, productNavActive, productNavItems } from '@burtson-labs/bandit-nav';
15
+ import {
16
+ ProductNav,
17
+ productNavActive,
18
+ productNavItems,
19
+ stealthAccessFromToken,
20
+ } from '@burtson-labs/bandit-nav';
12
21
 
13
- const items = productNavItems({ site: 'stealth', settingsPath: '/settings' });
22
+ const items = productNavItems({
23
+ site: 'stealth',
24
+ settingsPath: '/settings',
25
+ stealthAccess: stealthAccessFromToken(accessToken),
26
+ });
14
27
  const active = productNavActive(items, location.pathname);
15
28
 
16
29
  <ProductNav
@@ -24,9 +37,18 @@ const active = productNavActive(items, location.pathname);
24
37
  />;
25
38
  ```
26
39
 
27
- - `productNavItems({ site, settingsPath, chatOrigin?, stealthOrigin?, artifactsHref? })`
28
- returns the four items in display order. `artifactsHref` is for a host that
29
- serves the shared artifacts page itself (default: Stealth's page).
40
+ - `productNavItems({ site, settingsPath, chatOrigin?, stealthOrigin?, artifactsHref?, stealthAccess? })`
41
+ returns the items in display order: all four with `stealthAccess: true`,
42
+ Chat and Settings otherwise (the default, so an unknown session fails
43
+ closed). `artifactsHref` is for a host that serves the shared artifacts
44
+ page itself (default: Stealth's page).
45
+ - `stealthAccessFromToken(token)` / `stealthAccessFromClaims(claims)` apply
46
+ the access rule to the host's access token: true when it carries a
47
+ non-empty `stealthTenants` array (AuthApi stamps the user's active Stealth
48
+ memberships on platform tokens) or an organization session's `tenant_sso`.
49
+ The token is decoded, not verified: this decides what the bar shows, and
50
+ the Stealth APIs enforce membership themselves. Membership changes show
51
+ with the next token the host receives.
30
52
  - `productNavActive(items, pathname, aliases?)` picks the item the page
31
53
  belongs to; `aliases` adds host paths to a section (account pages under
32
54
  Settings, for instance).
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { PRODUCT_ORIGINS, productNavActive, productNavItems, type ProductNavItem, type ProductNavItemId, type ProductNavOptions, type ProductSite, } from './items.js';
1
+ export { PRODUCT_ORIGINS, STEALTH_SECTIONS, decodeJwtPayload, productNavActive, productNavItems, stealthAccessFromClaims, stealthAccessFromToken, type ProductNavItem, type ProductNavItemId, type ProductNavOptions, type ProductSite, } from './items.js';
2
2
  export { ProductNav, ensureProductNavStyles, type ProductNavProps } from './ProductNav.js';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { PRODUCT_ORIGINS, productNavActive, productNavItems, } from './items.js';
1
+ export { PRODUCT_ORIGINS, STEALTH_SECTIONS, decodeJwtPayload, productNavActive, productNavItems, stealthAccessFromClaims, stealthAccessFromToken, } from './items.js';
2
2
  export { ProductNav, ensureProductNavStyles } from './ProductNav.js';
package/dist/items.d.ts CHANGED
@@ -31,9 +31,35 @@ export interface ProductNavOptions {
31
31
  * page itself passes its own path.
32
32
  */
33
33
  artifactsHref?: string;
34
+ /**
35
+ * Whether the signed-in user may use Stealth (Agents and Artifacts): a
36
+ * member of at least one Stealth organization. Off or omitted, the bar is
37
+ * Chat and Settings only, so a user without Stealth never gets a link that
38
+ * dead-ends. Derive it with `stealthAccessFromToken` (or
39
+ * `stealthAccessFromClaims`); unknown means no.
40
+ */
41
+ stealthAccess?: boolean;
34
42
  }
35
- /** The canonical list, in display order. */
43
+ /** The Stealth-only sections, shown to members of a Stealth organization. */
44
+ export declare const STEALTH_SECTIONS: readonly ProductNavItemId[];
45
+ /**
46
+ * The canonical list, in display order: Chat, Agents, Artifacts, Settings with
47
+ * `stealthAccess`, Chat and Settings without it.
48
+ */
36
49
  export declare function productNavItems(options: ProductNavOptions): ProductNavItem[];
50
+ /**
51
+ * The access rule, from a token's claims: Stealth access is membership in a
52
+ * Stealth organization. AuthApi puts `stealthTenants` (the organizations
53
+ * where the user is an active member) on platform tokens, and an organization
54
+ * session token names its organization in `tenant_sso`. Anything else, or a
55
+ * value of the wrong shape, is no access. A display decision only: the
56
+ * Stealth APIs enforce membership themselves.
57
+ */
58
+ export declare function stealthAccessFromClaims(claims: unknown): boolean;
59
+ /** The payload of a JWT, unverified; null when it is not one. */
60
+ export declare function decodeJwtPayload(token: string | null | undefined): unknown;
61
+ /** `stealthAccessFromClaims` for the host's access token; no token is no access. */
62
+ export declare function stealthAccessFromToken(token: string | null | undefined): boolean;
37
63
  /**
38
64
  * Which item the current page belongs to: the local item whose path is the
39
65
  * pathname or one of its ancestors, or one of the host's extra paths for it
package/dist/items.js CHANGED
@@ -22,7 +22,12 @@ const hrefFor = (section, home, site, origin) => {
22
22
  const path = `/${SECTION_PATHS[section]}`;
23
23
  return home === site ? path : `${trimSlash(origin)}${path}`;
24
24
  };
25
- /** The canonical list, in display order. */
25
+ /** The Stealth-only sections, shown to members of a Stealth organization. */
26
+ export const STEALTH_SECTIONS = ['agents', 'artifacts'];
27
+ /**
28
+ * The canonical list, in display order: Chat, Agents, Artifacts, Settings with
29
+ * `stealthAccess`, Chat and Settings without it.
30
+ */
26
31
  export function productNavItems(options) {
27
32
  const chatOrigin = options.chatOrigin ?? PRODUCT_ORIGINS.chat;
28
33
  const stealthOrigin = options.stealthOrigin ?? PRODUCT_ORIGINS.stealth;
@@ -35,13 +40,54 @@ export function productNavItems(options) {
35
40
  ],
36
41
  ['settings', options.settingsPath],
37
42
  ];
38
- return entries.map(([id, href]) => ({
43
+ return entries
44
+ .filter(([id]) => options.stealthAccess === true || !STEALTH_SECTIONS.includes(id))
45
+ .map(([id, href]) => ({
39
46
  id,
40
47
  label: LABELS[id],
41
48
  href,
42
49
  external: !href.startsWith('/'),
43
50
  }));
44
51
  }
52
+ /**
53
+ * The access rule, from a token's claims: Stealth access is membership in a
54
+ * Stealth organization. AuthApi puts `stealthTenants` (the organizations
55
+ * where the user is an active member) on platform tokens, and an organization
56
+ * session token names its organization in `tenant_sso`. Anything else, or a
57
+ * value of the wrong shape, is no access. A display decision only: the
58
+ * Stealth APIs enforce membership themselves.
59
+ */
60
+ export function stealthAccessFromClaims(claims) {
61
+ if (!claims || typeof claims !== 'object')
62
+ return false;
63
+ const { stealthTenants, tenant_sso: orgSession } = claims;
64
+ if (Array.isArray(stealthTenants) &&
65
+ stealthTenants.some((tenant) => typeof tenant === 'string' && tenant.trim() !== '')) {
66
+ return true;
67
+ }
68
+ return typeof orgSession === 'string' && orgSession.trim() !== '';
69
+ }
70
+ /** The payload of a JWT, unverified; null when it is not one. */
71
+ export function decodeJwtPayload(token) {
72
+ if (typeof token !== 'string')
73
+ return null;
74
+ const part = token.split('.')[1];
75
+ if (!part)
76
+ return null;
77
+ try {
78
+ const base64 = part.replace(/-/g, '+').replace(/_/g, '/');
79
+ const binary = atob(base64 + '='.repeat((4 - (base64.length % 4)) % 4));
80
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
81
+ return JSON.parse(new TextDecoder().decode(bytes));
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /** `stealthAccessFromClaims` for the host's access token; no token is no access. */
88
+ export function stealthAccessFromToken(token) {
89
+ return stealthAccessFromClaims(decodeJwtPayload(token));
90
+ }
45
91
  /**
46
92
  * Which item the current page belongs to: the local item whose path is the
47
93
  * pathname or one of its ancestors, or one of the host's extra paths for it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burtson-labs/bandit-nav",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The Bandit product navigation (Chat, Agents, Artifacts, Settings) shared by banditailabs.com, stealth.banditailabs.com and the mobile apps.",
5
5
  "license": "Apache-2.0",
6
6
  "author": {