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

@ogcio/sag-client

v0.2.0

Published

Framework-agnostic client for the Secure API Gateway — authentication, health checks, and authenticated fetching

Readme

@ogcio/sag-client

Framework-agnostic TypeScript client for the Secure API Gateway. Handles authentication, health checks, and authenticated data fetching with built-in error handling for session expiry (401) and service unavailability (503).

Installation

pnpm add @ogcio/sag-client
# or
npm install @ogcio/sag-client

For React hooks, also install peer dependencies:

pnpm add react swr

Publishing

The package is published to npm as @ogcio/sag-client via release-please and Azure DevOps Pipelines.

How it works:

  1. Use Conventional Commits when committing changes to packages/sag-client/:
    • feat: ... — bumps a minor version
    • fix: ... — bumps a patch version
    • feat!: ... or BREAKING CHANGE: — bumps a major version (once past v1)
  2. When changes are merged to main, the pipeline runs release-please, which opens (or updates) a release PR that bumps the version in package.json, updates CHANGELOG.md, and updates .release-please-manifest.json
  3. When the release PR is merged, release-please creates a GitHub release and tag, and the pipeline publishes the package to npm

Configuration files (repo root):

  • release-please-config.json — release-please package configuration
  • .release-please-manifest.json — current version tracking
  • .azure/pipeline-sag-client.yaml — Azure DevOps pipeline (CI + release)

Entry Points

| Import path | Description | Dependencies | |---|---|---| | @ogcio/sag-client | Framework-agnostic core (SagClient class, auth functions, fetcher) | None (built-in fetch) | | @ogcio/sag-client/react | React hooks and provider | react >= 19, swr >= 2 (peer deps) |

Core API (@ogcio/sag-client)

SagClient class

import { SagClient } from "@ogcio/sag-client"

const client = new SagClient({
  gatewayUrl: "http://localhost:3333",
  appName: "cars",
  // Optional: custom handler for session expiry (default: redirect to sign-in)
  onSessionExpired: () => console.log("Session expired"),
})

// Auth
const status = await client.checkAuth()     // { authenticated, user, app, claims }
const health = await client.checkHealth()   // { available: boolean }
client.signIn()                             // POST-redirect to sign-in
client.signIn({ connector: "social:mygovid" })  // Direct sign-in via social connector
client.signIn({ redirectUrl: "/dashboard" })     // Explicit post-login redirect
client.signOut()                            // POST-redirect to sign-out
await client.invalidateSession()            // Clear server session (e.g. after onboarding)

// Organizations
const orgs = await client.fetchOrganizations()     // [{ id, name, description?, roles }]
await client.selectOrganization("org_xxx")          // Set current org (signed cookie)
const orgId = await client.getSelectedOrganization() // Get current org ID or null
await client.clearSelectedOrganization()             // Clear org selection

// Fetching
const { data } = await client.fetch<Car[]>("/cars")
const { data: orgData } = await client.fetch<Car[]>("/cars", { organizationId: "org_xxx" })

// Or create a reusable fetcher (for SWR, TanStack Query, etc.)
const fetcher = client.createFetcher<Car[]>()
const result = await fetcher("http://localhost:3333/cars")

Standalone functions

For one-off calls without creating a client instance:

import {
  checkAuth, checkHealth, signIn, signOut, invalidateSession,
  fetchOrganizations, selectOrganization, getSelectedOrganization, clearSelectedOrganization,
  createGatewayFetcher,
} from "@ogcio/sag-client"

const status = await checkAuth("http://localhost:3333")
const health = await checkHealth("http://localhost:3333")
signIn("http://localhost:3333", "cars")
signIn("http://localhost:3333", "cars", { connector: "social:mygovid" })
signOut("http://localhost:3333", "cars")
await invalidateSession("http://localhost:3333")

// Organization management
const orgs = await fetchOrganizations("http://localhost:3333")
await selectOrganization("http://localhost:3333", "org_xxx")
const orgId = await getSelectedOrganization("http://localhost:3333")
await clearSelectedOrganization("http://localhost:3333")

Error handling

The fetcher throws SagFetchError with status and code properties:

import { SagFetchError } from "@ogcio/sag-client"

try {
  await client.fetch("/cars")
} catch (err) {
  if (err instanceof SagFetchError) {
    console.log(err.status)  // 401, 503, etc.
    console.log(err.code)    // "SESSION_EXPIRED", "LOGTO_UNAVAILABLE", etc.
    console.log(err.message) // User-friendly message from the gateway
  }
}

M2M (Machine-to-Machine) requests

When the gateway service is configured for M2M, you can request that the gateway uses client_credentials instead of the user's session token by passing actorType: "m2m":

// Fetch with M2M token via SagClient
const { data } = await client.fetch<Notification[]>("/notifications", {
  actorType: "m2m",
})

// Or via the standalone fetcher factory
const fetcher = createGatewayFetcher("http://localhost:3333", "cars", undefined, {
  actorType: "m2m",
})

This sends the X-Request-Actor-Type: m2m header. The user must still be authenticated (M2M is not anonymous). The header name is also available as a constant:

import { ACTOR_TYPE_HEADER } from "@ogcio/sag-client"
// ACTOR_TYPE_HEADER === "X-Request-Actor-Type"

Organization-Scoped Requests

When the gateway has a selected organization (via cookie or explicit header), it uses getOrganizationToken(orgId) instead of the default getAccessToken(resource). You can pass organizationId to any fetch call:

// Via SagClient
const { data } = await client.fetch<Message[]>("/messaging", {
  organizationId: "org_xxx",
})

// Via standalone fetcher
const fetcher = createGatewayFetcher("http://localhost:3333", "messaging", undefined, {
  organizationId: "org_xxx",
})

This sends the X-Organization-Id: org_xxx header. The header name is available as a constant:

import { ORGANIZATION_ID_HEADER } from "@ogcio/sag-client"
// ORGANIZATION_ID_HEADER === "X-Organization-Id"

Role Detection Utilities

Pure functions for determining citizen / public-servant / onboarding status from Logto ID-token claims:

import {
  isCitizen,
  isPublicServant,
  isInactivePublicServant,
  isCitizenOnboarded,
  CONNECTOR_MYGOVID,
  CONNECTOR_ENTRAID,
  ALLOWED_SIGNIN_METHODS,
  DEFAULT_PUBLIC_SERVANT_ROLES,  // ["Organisation Admin", "Organisation Member"]
  ORG_ROLE_ADMIN,                // "Organisation Admin"
  ORG_ROLE_MEMBER,               // "Organisation Member"
} from "@ogcio/sag-client"

// After checking auth:
const status = await client.checkAuth()
if (status.authenticated) {
  const { organization_roles, roles } = status.claims

  // Use the platform defaults for citizen-facing apps:
  const citizen = isCitizen(organization_roles, DEFAULT_PUBLIC_SERVANT_ROLES)
  const publicServant = isPublicServant(organization_roles, DEFAULT_PUBLIC_SERVANT_ROLES)

  // Or use custom roles for admin apps:
  const isMessagingPS = isPublicServant(organization_roles, ["Messaging Public Servant"])

  const inactive = isInactivePublicServant(organization_roles)
  const onboarded = isCitizenOnboarded(roles)
}

Onboarding Helpers

Build redirect URLs for the onboarding and wrong-login-method flows:

import { buildOnboardingRedirectUrl, buildWrongLoginMethodRedirect } from "@ogcio/sag-client"

// Redirect to onboarding (after invalidating the session)
const url = buildOnboardingRedirectUrl({
  profileUrl: "https://profile.example.com",
  currentPath: "/messages/123",
  gatewayUrl: "http://localhost:3333",
  appName: "messaging",
  appBaseUrl: "https://messaging.example.com",  // where the user lands after auth
  connector: "social:mygovid",  // optional
})
window.location.href = url

// Redirect to wrong-login-method error page
const errorUrl = buildWrongLoginMethodRedirect({
  profileUrl: "https://profile.example.com",
  currentUrl: "https://messaging.example.com/messages/123",
})

Onboarding Flow (End-to-End)

For citizen-facing React applications, the recommended approach is the useOnboardingGuard hook (see the React API section below). It encapsulates all role checks, session invalidation, redirect URL construction, and infinite-loop prevention in a single call.

For non-React or server-side consumers, you can implement the same logic manually with the standalone helpers:

import {
  ALLOWED_SIGNIN_METHODS,
  CONNECTOR_MYGOVID,
  buildOnboardingRedirectUrl,
  buildWrongLoginMethodRedirect,
  isCitizen,
  isCitizenOnboarded,
} from "@ogcio/sag-client"

// After checking auth (e.g. in a page guard):
const status = await client.checkAuth()
if (!status.authenticated) return

const { roles, organization_roles } = status.claims

// 1. Wrong sign-in method → redirect to error page
const signinMethod = status.claims.signinMethod  // if available
if (signinMethod && !ALLOWED_SIGNIN_METHODS.includes(signinMethod)) {
  window.location.href = buildWrongLoginMethodRedirect({
    profileUrl: PROFILE_URL,
    currentUrl: window.location.href,
  })
  return
}

// 2. Not a citizen → skip (or redirect to admin app)
if (!isCitizen(organization_roles, PUBLIC_SERVANT_ROLES)) {
  return
}

// 3. Citizen but not onboarded → invalidate session and redirect
if (!isCitizenOnboarded(roles)) {
  await client.invalidateSession()
  window.location.href = buildOnboardingRedirectUrl({
    profileUrl: PROFILE_URL,
    currentPath: window.location.pathname,
    gatewayUrl: GATEWAY_URL,
    appName: APP_NAME,
    appBaseUrl: APP_BASE_URL,  // e.g. "http://localhost:3000"
    connector: CONNECTOR_MYGOVID,
  })
  return
}

// 4. Citizen is onboarded → proceed normally

The redirect chain is:

app → profile-service/onboarding?source=<gateway-sign-in-url>
   → (user completes onboarding, gains "Onboarded citizen" role)
   → GET gateway/auth/sign-in?app=<name>&redirectUrl=<app-url>&connector=social:mygovid
   → Logto OIDC flow (fresh claims)
   → gateway/auth/callback
   → app (original page, now with updated roles)

Note: appBaseUrl is the consuming application's URL (e.g. http://localhost:3000), not the gateway URL. This ensures the user lands back on the correct app page after the post-onboarding sign-in completes.

Types

import type {
  ActorType,                    // "user" | "m2m"
  AuthClaims,                   // { roles, organizations, organization_roles, signinMethod? }
  AuthStatus,
  AuthUser,
  GatewayFetchOptions,          // { actorType?, organizationId? }
  OrganizationInfo,             // { id, name, description?, roles }
  SagClientConfig,
  SignInOptions,                // { connector?, redirectUrl? }
  UseAuthResult,
  UseOnboardingGuardOptions,
  UseOnboardingGuardResult,
  UsePublicServantGuardOptions,
  UsePublicServantGuardResult,
  OnboardingRedirectParams,
  WrongLoginMethodParams,
} from "@ogcio/sag-client"

React API (@ogcio/sag-client/react)

Provider

Wrap your app with SagClientProvider:

import { SagClientProvider } from "@ogcio/sag-client/react"

export function Providers({ children }) {
  return (
    <SagClientProvider gatewayUrl="http://localhost:3333" appName="cars">
      {children}
    </SagClientProvider>
  )
}

Hooks

useAuth

import {
  useAuth,
  useSagClient,
  CONNECTOR_MYGOVID,
} from "@ogcio/sag-client/react"

function MyComponent() {
  const {
    authenticated,
    user,
    claims,           // { roles, organizations, organization_roles, signinMethod? }
    loading,
    logtoAvailable,
    signIn,           // accepts optional SignInOptions
    signOut,
    invalidateSession,
    refresh,
  } = useAuth()

  // Sign in with a specific connector
  const handleCitizenSignIn = () => signIn({ connector: CONNECTOR_MYGOVID })

  // Direct client access (advanced)
  const client = useSagClient()
}

useGatewayFetch

import { useGatewayFetch } from "@ogcio/sag-client/react"

// SWR-based data fetching
const { data, error, isLoading, refresh } = useGatewayFetch<Car[]>("/cars")

// Conditional fetching
const { data: detail } = useGatewayFetch<Car>(`/cars/${id}`, { enabled: !!id })

// M2M fetching (sends X-Request-Actor-Type: m2m header)
const { data: notifications } = useGatewayFetch<Notification[]>("/notifications", {
  actorType: "m2m",
})

useOnboardingGuard

Encapsulates the full citizen onboarding check for React applications. Must be used inside a SagClientProvider. Reads gatewayUrl and appName from the provider context automatically.

import {
  useOnboardingGuard,
  useAuth,
  CONNECTOR_MYGOVID,
} from "@ogcio/sag-client/react"

function ShellContent({ children }) {
  // publicServantRoles defaults to DEFAULT_PUBLIC_SERVANT_ROLES
  // (["Organisation Admin", "Organisation Member"])
  const { resolved } = useOnboardingGuard({
    profileUrl: "http://localhost:3001",
    appBaseUrl: "http://localhost:3000",
    connector: CONNECTOR_MYGOVID,   // optional
    // publicServantRoles: ["Custom Role"],  // override for admin apps
    // debounceMs: 30_000,                   // optional, default: 30000
  })

  const { user, signIn, signOut } = useAuth()

  if (!resolved) return <Loading />

  return user ? <App>{children}</App> : <SignInButton />
}

Options:

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | profileUrl | string | Yes | - | Profile service base URL | | appBaseUrl | string | Yes | - | Consuming application's base URL (for post-auth redirect) | | publicServantRoles | string[] | No | DEFAULT_PUBLIC_SERVANT_ROLES | Organisation roles that identify a public servant | | connector | string | No | undefined | Logto directSignIn connector (e.g. "social:mygovid") | | debounceMs | number | No | 30000 | Time window (ms) to prevent redirect loops |

Return value:

| Property | Type | Description | |----------|------|-------------| | resolved | boolean | true when the onboarding check has passed; false while loading or redirecting |

Behaviour:

  1. User is onboarded (isCitizenOnboarded) → resolved = true
  2. User is not a citizen → resolved = true (let them through)
  3. Recent redirect within debounceMsresolved = true (prevent loops)
  4. Wrong sign-in method → redirect to profile service error page
  5. Citizen not onboarded → invalidateSession() then redirect to profile service onboarding page

Important: Do not render data-fetching children (e.g. useGatewayFetch) until resolved is true to prevent API requests from racing with session invalidation.

usePublicServantGuard

Access guard hook for public-servant-facing (admin) applications. Checks if the authenticated user is an active public servant and optionally redirects unauthorized users.

import { usePublicServantGuard, useAuth } from "@ogcio/sag-client/react"

function AdminShell({ children }) {
  const { resolved, authorized, isInactive } = usePublicServantGuard({
    // Redirect citizens to the citizen-facing app (optional)
    unauthorizedRedirectUrl: "https://citizen-app.example.com",
    // Redirect inactive PS to a specific page (optional)
    inactiveRedirectUrl: "https://admin.example.com/inactive",
    // publicServantRoles: ["Custom Admin Role"],  // override defaults
  })

  const { user } = useAuth()

  if (!resolved) return <Loading />
  if (!authorized) return <AccessDenied />
  return <>{children}</>
}

Options:

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | publicServantRoles | string[] | No | DEFAULT_PUBLIC_SERVANT_ROLES | Organisation roles that identify an active public servant | | inactiveRedirectUrl | string | No | undefined | URL to redirect inactive PS users to. If omitted, they pass through with isInactive: true. | | unauthorizedRedirectUrl | string | No | undefined | URL to redirect non-PS users to. If omitted, they pass through with authorized: false. |

Return value:

| Property | Type | Description | |----------|------|-------------| | resolved | boolean | true once the guard check has completed | | authorized | boolean | true when the user is an active public servant | | isInactive | boolean | true when the user is an inactive public servant |

useGatewayFetch with organization scope

import { useGatewayFetch } from "@ogcio/sag-client/react"

// Fetch with an organization-scoped token
const { data } = useGatewayFetch<Message[]>("/messaging", {
  organizationId: "org_xxx",
})

Role detection utilities

Role detection functions are also re-exported from the React entry point for convenience:

import {
  isCitizen,
  isCitizenOnboarded,
  isPublicServant,
  CONNECTOR_MYGOVID,
  ALLOWED_SIGNIN_METHODS,
  DEFAULT_PUBLIC_SERVANT_ROLES,
  ORG_ROLE_ADMIN,
  ORG_ROLE_MEMBER,
  ONBOARDING_PATH,
  ONBOARDING_SOURCE_PARAM,
  WRONG_LOGIN_METHOD_PATH,
  WRONG_LOGIN_RETURN_URL_PARAM,
} from "@ogcio/sag-client/react"

Package Structure

src/
  index.ts            # Core entry point
  client.ts           # SagClient class
  auth.ts             # Standalone auth functions (signIn, signOut, invalidateSession, etc.)
  fetcher.ts          # Gateway fetcher factory
  roles.ts            # Role detection utilities (isCitizen, isPublicServant, etc.)
  onboarding.ts       # Onboarding & wrong-login-method URL builders
  types.ts            # All type definitions
  react/
    index.ts          # React entry point (re-exports roles & onboarding for convenience)
    provider.tsx      # SagClientProvider + useSagClient
    use-auth.ts       # useAuth hook (with claims, connector-aware signIn, invalidateSession)
    use-gateway-fetch.ts          # useGatewayFetch hook
    use-onboarding-guard.ts       # useOnboardingGuard hook (citizen onboarding guard)
    use-public-servant-guard.ts   # usePublicServantGuard hook (admin app access guard)