Shared Libraries — @progressivesurface/* SDK packages

How to consume PSI’s shared frontend packages, and the policy for versioning them. These are the Layer-1 building blocks of the PSI Web App Platform. Source repo: psi-platform on GHE.


What these are

A set of small, single-purpose npm packages published to GHE Packages (the private npm registry on our GitHub Enterprise instance) under the @progressivesurface scope. They replace per-app duplication: instead of every web app vendoring ps.css and re-deriving its MSAL config, apps install the package.

Why @progressivesurface and not @psi? GHE/GitHub Packages require the npm scope to equal the owning org login, and the org is ProgressiveSurface. There is no psi org, so @psi/* cannot be published there. See psi-platform/docs/decisions/0002-package-scope.md.

PackageOwnsStatus
@progressivesurface/uiDesign-system tokens (ps.css), React component wrappers (PSIButton, PSITile, PSIKpi), typed icon registry (PSIcon)live
@progressivesurface/authMSAL/Entra bootstrap (createMsalConfig, initializeMsal), useAuth, <AuthGuard>, plus a /server token-validator exportlive
@progressivesurface/accessFeature flags + channel/RBAC gateslive
@progressivesurface/broadcastCross-tab / cross-app broadcast messaging SDKlive
@progressivesurface/grid<PSIDataGrid /> — KendoReact behind a PSI-owned API. Never import KendoReact directly; the consuming app activates the Telerik licence at build time (TELERIK_LICENSE org secret).live
@progressivesurface/bffBackend-for-frontend helpers (Express 5 SPA fallback, /api/*-scoped auth gate, chat router)live
@progressivesurface/webbotsAsk-the-Fleet chat framework (ChatSidebar + friends)live
@progressivesurface/dataTyped clients routed through the Portal gatewayplanned
@progressivesurface/{telemetry,layout,dev,cli}App Insights, PSIPortalShell, Vite plugin, scaffoldingplanned

Version numbers are deliberately not listed here — they moved every few days and this table was wrong within a fortnight. For the current version of any package, read its package.json / CHANGELOG.md in psi-platform, or the package page on GHE Packages. “Planned” packages do not exist on disk yet, so psi-cli new is not runnable.

Owner: Adam Devereaux (adevereaux@progressivesurface.com). Breaking (major) changes require owner sign-off.

Starting a NEW app? Don’t hand-assemble one. psi-platform/templates/default is a working Vite + React + TypeScript scaffold with auth, design tokens, feature gating, a PR-time CI gate, and coverage thresholds already wired — copy it out, or let the create-webapp Claude Code skill drive it and hand off to deploy-webapp for Azure. (psi-cli new is still unbuilt; the template is the available-now path.) The scaffold lives inside the pnpm workspace, so pnpm -r build/test cover it and it can’t rot into a state that doesn’t compile.

Contributing to the packages (as opposed to consuming them): start with psi-platform/CLAUDE_ONBOARDING.md — the cold-start guide covering the release flow (Changesets Version-PR; never hand-set a version), the workspace:^ rule, and the fact that a push to main publishes, so everything goes through a PR. main is branch-protected and requires the validate check. Then AGENTS.md for conventions.


Consuming the packages

These are private packages — every consumer authenticates; there is no anonymous read. The setup is identical for every repo and every package, and you do it once per repo (committed) and once per developer machine. A single token covers all current and future @progressivesurface/* packages across all repos, because auth is keyed by the registry host, not per package.

1. Per repo — commit an .npmrc (no secret)

@progressivesurface:registry=https://npm.progressivesurface.ghe.com
//npm.progressivesurface.ghe.com/:_authToken=${NODE_AUTH_TOKEN}

The token is supplied by the NODE_AUTH_TOKEN env var at install time — this file is safe to commit.

2. CI — zero developer tokens

The workflow’s built-in GITHUB_TOKEN is accepted by the registry. Grant it package read and pass it as NODE_AUTH_TOKEN:

permissions:
  contents: read
  packages: read          # lets npm read @progressivesurface/* from GHE Packages
jobs:
  build:
    steps:
      - uses: actions/checkout@v4
      - run: npm ci         # or pnpm install
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

No PAT, no per-repo secret. (PSI Portal’s deploy.yml is the reference.)

⚠️ Critical caveat — the packages (and their source repo) MUST be internal. A repo’s job GITHUB_TOKEN is scoped to that repo only. These npm packages are repository-scoped — they inherit access from their source repo (psi-platform). If psi-platform (and therefore the packages) is private, a different repo’s GITHUB_TOKEN cannot read themnpm ci fails with 403 ... permission_denied: read_package, even with packages: read. This silently broke every psi-portal deploy for ~4 weeks (2026-06-01 → 06-29); the earlier “fix” PRs were only verified locally, where a classic PAT (with repo scope) masked the gap.

The fix: set the psi-platform repo → internal, then each package → internal (a package can’t go internal while its source repo is private). With the packages internal, any org repo’s GITHUB_TOKEN reads them and the snippet above works as written.

New packages: there is no API to change package visibility (REST PATCH 404s; no GraphQL mutation) — it’s a UI-only toggle. So don’t script it; instead publish from the now-internal psi-platform and new packages inherit internal from birth. Confirm on first publish.

3. Local dev — once per machine

⚠️ gh auth token does NOT work here. The GitHub Packages npm registry rejects the gh CLI’s OAuth token (403 … does not match expected scopes), even when it has read:packages. You need a classic PAT.

  1. Create the token in your personal settings — not the org’s Settings tab (looking at the org is why people can’t find it). Go directly to https://progressivesurface.ghe.com/settings/tokens (or avatar → Settings → Developer settings) → Tokens (classic) → Generate. Then:
    • Select both repo and read:packages — the packages are private and tied to the private psi-platform repo, so read:packages alone returns 403.
    • Configure SSO → Authorize → ProgressiveSurface. This org enforces SAML SSO; an un-authorized token 403s even with the right scopes. (Most-missed step.)
  2. Store it once. The easiest way is the helper script in the psi-platform repo, which writes both the scope→registry mapping and the auth token to your user ~/.npmrc (idempotent, UTF-8 no BOM) and verifies read access:
    ./scripts/setup-npm-auth.ps1        # PowerShell  (or setup-npm-auth.sh)
    Or do it by hand — this covers every @progressivesurface/* package in every repo, forever:
    npm config set @progressivesurface:registry "https://npm.progressivesurface.ghe.com" --location=user
    npm config set //npm.progressivesurface.ghe.com/:_authToken "<ghp_token>" --location=user
  3. npm install / pnpm install now resolves the packages.

See the step-by-step guide in psi-platform/docs/guides/installing-psi-packages.md.

4. Use

// app entry — load the design system once (stop vendoring ps.css)
import '@progressivesurface/ui/css';
 
import { PSIButton, PSIKpi } from '@progressivesurface/ui';
import { createMsalConfig, initializeMsal, AuthGuard, useAuth } from '@progressivesurface/auth';

@progressivesurface/auth’s defaults encode the Web App Compliance Standard: redirect flow, localStorage token cache, silent-acquire-then-redirect recovery, and a 5-minute ID-token refresh guard. See each package’s README.md for the full API.

New apps created with psi-cli new (Phase 5) will scaffold the .npmrc and the CI block automatically — this manual setup is only for existing repos.


Versioning policy

  • SemVer, managed with Changesets. Every change adds a changeset (pnpm changeset) describing the bump.
  • Patch — bug fixes, no API change. Adopt freely.
  • Minor — additive (new component, new option). Backward compatible.
  • Major — breaking. Requires owner sign-off and a coordinated migration note; consuming apps upgrade deliberately, not automatically.
  • Apps should pin a caret range (^0.1.0) so they get patches/minors but not majors.
  • @progressivesurface/ui depends on @progressivesurface/design-system (published by the psi-design-system repo since v1.4.0) and re-ships its CSS
    • fonts in dist/ at build time — nothing is vendored, so ui can’t silently drift from the design system. When the design system releases, Dependabot proposes the bump in psi-platform; merging it plus a changeset ships the new ui. Check packages/ui/package.json for the version in use — never a number quoted in a doc.

Publishing (maintainers)

pnpm changeset            # describe the change
pnpm version-packages     # changeset version → bumps + CHANGELOG
# merge to main → GH Actions publishes to GHE Packages and tags @progressivesurface/<pkg>@x.y.z

The publish workflow is identity-based (packages: write on the job GITHUB_TOKEN); no personal token is used to publish.


Known bad versions

  • @progressivesurface/grid < 0.3.1 — “Export to Excel” downloads a blank workbook. <PSIDataGrid excelExport> / <PSIDataTable /> wrote every row with zero cells: the file is valid and correctly named, opens empty, and nothing errors, so the symptom looks like a data problem rather than a grid bug. Fixed in 0.3.1; any app still on 0.3.0 needs a lockfile bump (npm install @progressivesurface/grid@^0.3.1), not a code change.

Migration guidance

Adopt opportunistically — the next time an app touches CSS or auth, swap to the package and delete the duplicated code. PSI Portal is the reference migration (it replaced its vendored ps.css and ~250 lines of MSAL logic). App-specific configuration (Graph scopes, Entra group IDs) stays in the app; only the duplicated logic moves to the package.