npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@4cloudguru/cloud-suite-ui

v0.13.1

Published

Shared UI foundation (theme, identity, app shell, and cross-cutting components) for the Terraform suite frontends.

Readme

@4cloudguru/cloud-suite-ui

Shared UI foundation for the Terraform suite frontends (terraform-registry-frontend and terraform-state-manager-frontend). It centralises the look-and-feel and cross-cutting behaviour so both apps stay in visual and behavioural parity from a single source of truth.

What's inside

| Area | Exports | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Tokens | BRAND_PRIMARY, SECONDARY_LIGHT, SECONDARY_DARK, dark surfaces, font stack, BORDER_RADIUS, RTL_LANGUAGES | | Theme | createAppTheme(mode, prefersReducedMotion, direction, overrides), SuiteThemeProvider, useThemeMode | | Identity | AuthProvider (parameterised by an AuthApi), useAuth (returns hasScope, memberships, organizationChoices, currentOrganizationId, setCurrentOrganization), ADMIN_SCOPE, SESSION_WARNING_LEAD_MS, SessionExpiryWarning, ORGANIZATION_HEADER, DEFAULT_ORGANIZATION_KEY, resolveCurrentOrganization, shouldOfferOrganizationChoice, actingOrganizationChoices, types | | Consent | ConsentProvider, useConsent, ConsentBanner | | Components | PageHeader, DashboardCard, Page, NotificationChannelsSection, ApiKeyExpirySettingsCard, BrandingSettingsCard (requires a host-supplied validators prop) | | Shell | SuiteLayout (parameterised by nav + branding + auth), SuiteSwitcher, OrganizationPicker, nav types | | Utils | isSafeUrl (host-supplied URL guard for navigation / image sinks) |

Acting organization

A user who belongs to more than one organization has to say which one a write belongs to. AuthProvider resolves that from their memberships plus a remembered choice, and exposes it as currentOrganizationId; OrganizationPicker renders the choice when there is one.

Three properties are worth knowing before wiring it:

  • A single-organization deployment is unchanged. With one membership the selection is implied, OrganizationPicker renders nothing, and no header has to be sent for writes to work.
  • The remembered choice is a hint, never an authority. It selects a membership only when it matches one the server just returned, so a hand-edited value — or one left behind by a different user of the same browser — is discarded rather than honoured. The key is also cleared on sign-out.
  • Switching re-resolves the session. allowed_scopes is the effective set for the selected organization, so setCurrentOrganization performs the fresh getCurrentUser() that MeResponse.allowed_scopes tells hosts they must, rather than leaving stale scopes in place.
  • A platform administrator is not a member of anything. They reach every organization and belong to none, so their memberships are the wrong universe to derive a choice from: the server insists such a caller names one, and a membership-driven picker offers them nothing to name. Pass the organizations they may act in as selectableOrganizations and the picker offers those; organizationChoices is the resulting union and is what OrganizationPicker renders and what setCurrentOrganization validates against. Omit the prop and every existing behaviour is unchanged, because the union is then the memberships themselves. It is a display universe, not a grant — the server still refuses any organization the caller may not reach.

Send the selection on every request as ORGANIZATION_HEADER (X-Organization-Id). The same name is defined server-side in terraform-suite-identity's identity/tenantscope. It is a claim: the server verifies it against a scope it resolved itself and refuses anything the caller may not reach, so this is not an authorization boundary.

Framework packages (React, MUI, Emotion, i18next, react-router) are peer dependencies — the consuming app provides a single copy at runtime.

Develop

Requires Node >=22.0.0 <25 (see engines in package.json).

npm install
npm run typecheck   # tsc --noEmit
npm test            # vitest
npm run build       # tsup -> dist/ (ESM + .d.ts)

Publishing (GitHub Packages)

Publishing is automated by .github/workflows/publish.yml: create a GitHub Release tagged vX.Y.Z (matching package.json) and the workflow builds, type-checks, tests, and runs npm publish to https://npm.pkg.github.com using the repo's GITHUB_TOKEN.

Integrity guarantees for consumers:

  • The publish job refuses to publish unless the triggering ref is exactly the git tag matching package.json's version — a manual workflow_dispatch run against an arbitrary branch is rejected, not just discouraged. This tag/version check is the guarantee enforced by code in this repository. The job also targets a GitHub Environment (release); any human-review gate on that environment must be configured as a required-reviewer rule in repo Settings (not tracked in git), so independent-review protection is not guaranteed by this repository alone.

  • Before npm publish, CI asserts the tarball (npm pack --dry-run) only contains dist/ plus package.json/README.md/LICENSE/NOTICE — no source, tests, or config files ship.

  • Every release generates GitHub Artifact Attestations — build provenance plus a CycloneDX SBOM — bound to the exact published npm tarball. Verify by fetching the tarball and checking it:

    npm pack @4cloudguru/cloud-suite-ui
    gh attestation verify --repo 4cloudguru/cloud-suite-ui ./4cloudguru-cloud-suite-ui-*.tgz

    Releases also carry npm provenance, verifiable without the gh CLI:

    npm audit signatures
  • CI runs npm audit --audit-level=moderate on every push/PR, and CodeQL (javascript-typescript) runs on every push/PR plus a weekly schedule (see .github/workflows/codeql.yml).

  • A commitlint check on every PR enforces Conventional Commits, since release-please derives version bumps solely from commit messages.

See SECURITY.md for the vulnerability disclosure policy.

Consuming (in each frontend)

The package is public on the npm registry, so no .npmrc or auth token is needed:

npm install @4cloudguru/cloud-suite-ui
import { SuiteThemeProvider, PageHeader, useAuth } from '@4cloudguru/cloud-suite-ui'

This package is a build-time dependency only; each app remains independently deployable. Wiring the two apps to consume it is intentionally a separate step.

This package is ESM-only ("type": "module", a single import export condition, no require) — consume it from an ESM build/toolchain. It also declares engines.node (>=22.0.0 <25); installing under an older/newer Node major is unsupported.

Internationalization (i18n)

Every component resolves user-facing copy through useTranslation()'s t(key, { defaultValue }), so a host app's i18next configuration owns translation and an incomplete bundle still renders readable English.

BrandingSettingsCard layers a host-supplied strings prop on top of that same contract, for apps that already have translated copy for their own field labels/help text and would rather pass it straight through than duplicate it into an i18next bundle. Precedence per field is strings.fields[key]?.label ?? t('branding.fields.<key>.label', { defaultValue: '<English label>' }) (and the same for helperText) — an app that supplies no strings entry for a field still gets a translatable label/helper text via t(); supplying an entry with e.g. errorText but no helperText intentionally renders no helper at all (the host is presumed to own that field's copy outright), rather than falling back to the t()-resolved English.

See CONTRIBUTING.md for the prop-contract stability convention that applies to BrandingSettingsCard, NotificationChannelsSection, ApiKeyExpirySettingsCard, and UIThemeConfig specifically — a separate concern from this section's translation contract, which applies uniformly to every component.

Security model

  • Token custody is the host app's responsibility. AuthProvider is parameterised by an AuthApi your app implements (getCurrentUser/login/logout/refreshToken/etc.) — this library never reads or writes a token/cookie itself. Prefer an HttpOnly cookie over storing a bearer token in localStorage/sessionStorage if your backend supports it.

  • onClearStorage is how you clear YOUR app's cached auth data when the session ends — on explicit logout AND when the session fails closed (a 401, a lapsed session, or a malformed /me response). Pass it whenever your app caches anything auth-related (a bearer token, query data keyed to the signed-in user) outside of AuthProvider's own React state. One deliberate carve-out: a /me that returns 200 with a session_expires_at already in the past is read as a disagreement between the client and server clocks, not as an expiry — no expiry is scheduled, onClearStorage does not fire, and a console.warn names the skew. Otherwise a browser clock running ahead of the server would lock the user out on every /me (#178).

  • hasScope/allowedScopes are UI-visibility gates only — NOT an authorization boundary. They hide/show nav items and affordances client-side; every backend endpoint must independently re-enforce authorization on every request regardless of what the client believes. The special ADMIN_SCOPE ('admin') wildcard mirrors the backend's own admin-wildcard convention — do not rely on it as a security control in this library.

  • refreshSession() logs out on failure (a failed token refresh clears the session rather than leaving a stale/ambiguous state); authError on the auth context is a sanitized, display-safe string describing the most recent failed session-resolution call — never the raw error object, response body, headers, or URLs — if your app wants to distinguish a network blip from a real "not logged in" state.

  • Pass an app-specific storageKey to ConsentProvider/SuiteThemeProvider, and an app-specific groupStateStorageKey to SuiteLayout, if your app shares an origin with a sibling suite app — the default keys are generic and will collide otherwise (all three log a one-time console warning if you don't). SuiteLayout clears its own persisted nav-group state on every transition to unauthenticated; consent preferences deliberately survive, since a consent decision is origin-scoped rather than session-scoped.

  • isSafeUrl is the URL guard the shared components apply to host-supplied URLs before using them for navigation (SuiteSwitcher) or image sinks (SuiteLayout/SuiteThemeProvider branding). It is exported so your app can apply the same allowlist (http/https/mailto/tel and relative paths only) to any backend- or user-influenced URL at its own boundary. Compose rather than re-derive it — an app that needs a narrower rule should call isSafeUrl first and layer its own check on top, so a future fix to the shared parsing logic reaches every consumer:

    export function isSafeExternalUrl(value: string | null | undefined): value is string {
      if (!isSafeUrl(value)) return false // shared base allowlist + normalisation
      if (/^[/#.]/.test(value.trim())) return true // relative — already screened above
      return new URL(value.trim()).protocol === 'https:' // app-specific narrowing
    }
  • Route props (SuiteLayout's NavItem.path and loginPath, DashboardCard's to) must be in-app paths beginning with /. Anything absolute or protocol-relative is rejected with a console warning and falls back to /.