@ogcio/sag-client 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 +533 -0
- package/dist/auth.d.ts +55 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +154 -0
- package/dist/auth.js.map +1 -0
- package/dist/client.d.ts +93 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +118 -0
- package/dist/client.js.map +1 -0
- package/dist/fetcher.d.ts +39 -0
- package/dist/fetcher.d.ts.map +1 -0
- package/dist/fetcher.js +141 -0
- package/dist/fetcher.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/onboarding.d.ts +68 -0
- package/dist/onboarding.d.ts.map +1 -0
- package/dist/onboarding.js +67 -0
- package/dist/onboarding.js.map +1 -0
- package/dist/react/index.d.ts +15 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +16 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/provider.d.ts +22 -0
- package/dist/react/provider.d.ts.map +1 -0
- package/dist/react/provider.js +31 -0
- package/dist/react/provider.js.map +1 -0
- package/dist/react/use-auth.d.ts +13 -0
- package/dist/react/use-auth.d.ts.map +1 -0
- package/dist/react/use-auth.js +84 -0
- package/dist/react/use-auth.js.map +1 -0
- package/dist/react/use-gateway-fetch.d.ts +29 -0
- package/dist/react/use-gateway-fetch.d.ts.map +1 -0
- package/dist/react/use-gateway-fetch.js +47 -0
- package/dist/react/use-gateway-fetch.js.map +1 -0
- package/dist/react/use-gateway-mutation.d.ts +32 -0
- package/dist/react/use-gateway-mutation.d.ts.map +1 -0
- package/dist/react/use-gateway-mutation.js +58 -0
- package/dist/react/use-gateway-mutation.js.map +1 -0
- package/dist/react/use-onboarding-guard.d.ts +91 -0
- package/dist/react/use-onboarding-guard.d.ts.map +1 -0
- package/dist/react/use-onboarding-guard.js +140 -0
- package/dist/react/use-onboarding-guard.js.map +1 -0
- package/dist/react/use-public-servant-guard.d.ts +70 -0
- package/dist/react/use-public-servant-guard.d.ts.map +1 -0
- package/dist/react/use-public-servant-guard.js +79 -0
- package/dist/react/use-public-servant-guard.js.map +1 -0
- package/dist/roles.d.ts +67 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +93 -0
- package/dist/roles.js.map +1 -0
- package/dist/types.d.ts +131 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +28 -0
- package/dist/types.js.map +1 -0
- package/package.json +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
# @ogcio/sag-client
|
|
2
|
+
|
|
3
|
+
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).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @ogcio/sag-client
|
|
9
|
+
# or
|
|
10
|
+
npm install @ogcio/sag-client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For React hooks, also install peer dependencies:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add react swr
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Publishing
|
|
20
|
+
|
|
21
|
+
The package is published to npm as `@ogcio/sag-client` via [release-please](https://github.com/googleapis/release-please) and Azure DevOps Pipelines.
|
|
22
|
+
|
|
23
|
+
**How it works:**
|
|
24
|
+
|
|
25
|
+
1. Use [Conventional Commits](https://www.conventionalcommits.org/) when committing changes to `packages/sag-client/`:
|
|
26
|
+
- `feat: ...` — bumps a minor version
|
|
27
|
+
- `fix: ...` — bumps a patch version
|
|
28
|
+
- `feat!: ...` or `BREAKING CHANGE:` — bumps a major version (once past v1)
|
|
29
|
+
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`
|
|
30
|
+
3. When the release PR is merged, release-please creates a GitHub release and tag, and the pipeline publishes the package to npm
|
|
31
|
+
|
|
32
|
+
**Configuration files (repo root):**
|
|
33
|
+
- `release-please-config.json` — release-please package configuration
|
|
34
|
+
- `.release-please-manifest.json` — current version tracking
|
|
35
|
+
- `.azure/pipeline-sag-client.yaml` — Azure DevOps pipeline (CI + release)
|
|
36
|
+
|
|
37
|
+
## Entry Points
|
|
38
|
+
|
|
39
|
+
| Import path | Description | Dependencies |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `@ogcio/sag-client` | Framework-agnostic core (SagClient class, auth functions, fetcher) | None (built-in `fetch`) |
|
|
42
|
+
| `@ogcio/sag-client/react` | React hooks and provider | `react` >= 19, `swr` >= 2 (peer deps) |
|
|
43
|
+
|
|
44
|
+
## Core API (`@ogcio/sag-client`)
|
|
45
|
+
|
|
46
|
+
### SagClient class
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { SagClient } from "@ogcio/sag-client"
|
|
50
|
+
|
|
51
|
+
const client = new SagClient({
|
|
52
|
+
gatewayUrl: "http://localhost:3333",
|
|
53
|
+
appName: "cars",
|
|
54
|
+
// Optional: custom handler for session expiry (default: redirect to sign-in)
|
|
55
|
+
onSessionExpired: () => console.log("Session expired"),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Auth
|
|
59
|
+
const status = await client.checkAuth() // { authenticated, user, app, claims }
|
|
60
|
+
const health = await client.checkHealth() // { available: boolean }
|
|
61
|
+
client.signIn() // POST-redirect to sign-in
|
|
62
|
+
client.signIn({ connector: "social:mygovid" }) // Direct sign-in via social connector
|
|
63
|
+
client.signIn({ redirectUrl: "/dashboard" }) // Explicit post-login redirect
|
|
64
|
+
client.signOut() // POST-redirect to sign-out
|
|
65
|
+
await client.invalidateSession() // Clear server session (e.g. after onboarding)
|
|
66
|
+
|
|
67
|
+
// Organizations
|
|
68
|
+
const orgs = await client.fetchOrganizations() // [{ id, name, description?, roles }]
|
|
69
|
+
await client.selectOrganization("org_xxx") // Set current org (signed cookie)
|
|
70
|
+
const orgId = await client.getSelectedOrganization() // Get current org ID or null
|
|
71
|
+
await client.clearSelectedOrganization() // Clear org selection
|
|
72
|
+
|
|
73
|
+
// Fetching
|
|
74
|
+
const { data } = await client.fetch<Car[]>("/cars")
|
|
75
|
+
const { data: orgData } = await client.fetch<Car[]>("/cars", { organizationId: "org_xxx" })
|
|
76
|
+
|
|
77
|
+
// Or create a reusable fetcher (for SWR, TanStack Query, etc.)
|
|
78
|
+
const fetcher = client.createFetcher<Car[]>()
|
|
79
|
+
const result = await fetcher("http://localhost:3333/cars")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Standalone functions
|
|
83
|
+
|
|
84
|
+
For one-off calls without creating a client instance:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import {
|
|
88
|
+
checkAuth, checkHealth, signIn, signOut, invalidateSession,
|
|
89
|
+
fetchOrganizations, selectOrganization, getSelectedOrganization, clearSelectedOrganization,
|
|
90
|
+
createGatewayFetcher,
|
|
91
|
+
} from "@ogcio/sag-client"
|
|
92
|
+
|
|
93
|
+
const status = await checkAuth("http://localhost:3333")
|
|
94
|
+
const health = await checkHealth("http://localhost:3333")
|
|
95
|
+
signIn("http://localhost:3333", "cars")
|
|
96
|
+
signIn("http://localhost:3333", "cars", { connector: "social:mygovid" })
|
|
97
|
+
signOut("http://localhost:3333", "cars")
|
|
98
|
+
await invalidateSession("http://localhost:3333")
|
|
99
|
+
|
|
100
|
+
// Organization management
|
|
101
|
+
const orgs = await fetchOrganizations("http://localhost:3333")
|
|
102
|
+
await selectOrganization("http://localhost:3333", "org_xxx")
|
|
103
|
+
const orgId = await getSelectedOrganization("http://localhost:3333")
|
|
104
|
+
await clearSelectedOrganization("http://localhost:3333")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Error handling
|
|
108
|
+
|
|
109
|
+
The fetcher throws `SagFetchError` with `status` and `code` properties:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { SagFetchError } from "@ogcio/sag-client"
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await client.fetch("/cars")
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (err instanceof SagFetchError) {
|
|
118
|
+
console.log(err.status) // 401, 503, etc.
|
|
119
|
+
console.log(err.code) // "SESSION_EXPIRED", "LOGTO_UNAVAILABLE", etc.
|
|
120
|
+
console.log(err.message) // User-friendly message from the gateway
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### M2M (Machine-to-Machine) requests
|
|
126
|
+
|
|
127
|
+
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"`:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// Fetch with M2M token via SagClient
|
|
131
|
+
const { data } = await client.fetch<Notification[]>("/notifications", {
|
|
132
|
+
actorType: "m2m",
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Or via the standalone fetcher factory
|
|
136
|
+
const fetcher = createGatewayFetcher("http://localhost:3333", "cars", undefined, {
|
|
137
|
+
actorType: "m2m",
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
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:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { ACTOR_TYPE_HEADER } from "@ogcio/sag-client"
|
|
145
|
+
// ACTOR_TYPE_HEADER === "X-Request-Actor-Type"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Organization-Scoped Requests
|
|
149
|
+
|
|
150
|
+
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:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Via SagClient
|
|
154
|
+
const { data } = await client.fetch<Message[]>("/messaging", {
|
|
155
|
+
organizationId: "org_xxx",
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Via standalone fetcher
|
|
159
|
+
const fetcher = createGatewayFetcher("http://localhost:3333", "messaging", undefined, {
|
|
160
|
+
organizationId: "org_xxx",
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
This sends the `X-Organization-Id: org_xxx` header. The header name is available as a constant:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { ORGANIZATION_ID_HEADER } from "@ogcio/sag-client"
|
|
168
|
+
// ORGANIZATION_ID_HEADER === "X-Organization-Id"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Role Detection Utilities
|
|
172
|
+
|
|
173
|
+
Pure functions for determining citizen / public-servant / onboarding status from Logto ID-token claims:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import {
|
|
177
|
+
isCitizen,
|
|
178
|
+
isPublicServant,
|
|
179
|
+
isInactivePublicServant,
|
|
180
|
+
isCitizenOnboarded,
|
|
181
|
+
CONNECTOR_MYGOVID,
|
|
182
|
+
CONNECTOR_ENTRAID,
|
|
183
|
+
ALLOWED_SIGNIN_METHODS,
|
|
184
|
+
DEFAULT_PUBLIC_SERVANT_ROLES, // ["Organisation Admin", "Organisation Member"]
|
|
185
|
+
ORG_ROLE_ADMIN, // "Organisation Admin"
|
|
186
|
+
ORG_ROLE_MEMBER, // "Organisation Member"
|
|
187
|
+
} from "@ogcio/sag-client"
|
|
188
|
+
|
|
189
|
+
// After checking auth:
|
|
190
|
+
const status = await client.checkAuth()
|
|
191
|
+
if (status.authenticated) {
|
|
192
|
+
const { organization_roles, roles } = status.claims
|
|
193
|
+
|
|
194
|
+
// Use the platform defaults for citizen-facing apps:
|
|
195
|
+
const citizen = isCitizen(organization_roles, DEFAULT_PUBLIC_SERVANT_ROLES)
|
|
196
|
+
const publicServant = isPublicServant(organization_roles, DEFAULT_PUBLIC_SERVANT_ROLES)
|
|
197
|
+
|
|
198
|
+
// Or use custom roles for admin apps:
|
|
199
|
+
const isMessagingPS = isPublicServant(organization_roles, ["Messaging Public Servant"])
|
|
200
|
+
|
|
201
|
+
const inactive = isInactivePublicServant(organization_roles)
|
|
202
|
+
const onboarded = isCitizenOnboarded(roles)
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Onboarding Helpers
|
|
207
|
+
|
|
208
|
+
Build redirect URLs for the onboarding and wrong-login-method flows:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { buildOnboardingRedirectUrl, buildWrongLoginMethodRedirect } from "@ogcio/sag-client"
|
|
212
|
+
|
|
213
|
+
// Redirect to onboarding (after invalidating the session)
|
|
214
|
+
const url = buildOnboardingRedirectUrl({
|
|
215
|
+
profileUrl: "https://profile.example.com",
|
|
216
|
+
currentPath: "/messages/123",
|
|
217
|
+
gatewayUrl: "http://localhost:3333",
|
|
218
|
+
appName: "messaging",
|
|
219
|
+
appBaseUrl: "https://messaging.example.com", // where the user lands after auth
|
|
220
|
+
connector: "social:mygovid", // optional
|
|
221
|
+
})
|
|
222
|
+
window.location.href = url
|
|
223
|
+
|
|
224
|
+
// Redirect to wrong-login-method error page
|
|
225
|
+
const errorUrl = buildWrongLoginMethodRedirect({
|
|
226
|
+
profileUrl: "https://profile.example.com",
|
|
227
|
+
currentUrl: "https://messaging.example.com/messages/123",
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Onboarding Flow (End-to-End)
|
|
232
|
+
|
|
233
|
+
For citizen-facing **React** applications, the recommended approach is the `useOnboardingGuard` hook (see the [React API](#react-api-ogciosag-clientreact) section below). It encapsulates all role checks, session invalidation, redirect URL construction, and infinite-loop prevention in a single call.
|
|
234
|
+
|
|
235
|
+
For **non-React** or server-side consumers, you can implement the same logic manually with the standalone helpers:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import {
|
|
239
|
+
ALLOWED_SIGNIN_METHODS,
|
|
240
|
+
CONNECTOR_MYGOVID,
|
|
241
|
+
buildOnboardingRedirectUrl,
|
|
242
|
+
buildWrongLoginMethodRedirect,
|
|
243
|
+
isCitizen,
|
|
244
|
+
isCitizenOnboarded,
|
|
245
|
+
} from "@ogcio/sag-client"
|
|
246
|
+
|
|
247
|
+
// After checking auth (e.g. in a page guard):
|
|
248
|
+
const status = await client.checkAuth()
|
|
249
|
+
if (!status.authenticated) return
|
|
250
|
+
|
|
251
|
+
const { roles, organization_roles } = status.claims
|
|
252
|
+
|
|
253
|
+
// 1. Wrong sign-in method → redirect to error page
|
|
254
|
+
const signinMethod = status.claims.signinMethod // if available
|
|
255
|
+
if (signinMethod && !ALLOWED_SIGNIN_METHODS.includes(signinMethod)) {
|
|
256
|
+
window.location.href = buildWrongLoginMethodRedirect({
|
|
257
|
+
profileUrl: PROFILE_URL,
|
|
258
|
+
currentUrl: window.location.href,
|
|
259
|
+
})
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 2. Not a citizen → skip (or redirect to admin app)
|
|
264
|
+
if (!isCitizen(organization_roles, PUBLIC_SERVANT_ROLES)) {
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 3. Citizen but not onboarded → invalidate session and redirect
|
|
269
|
+
if (!isCitizenOnboarded(roles)) {
|
|
270
|
+
await client.invalidateSession()
|
|
271
|
+
window.location.href = buildOnboardingRedirectUrl({
|
|
272
|
+
profileUrl: PROFILE_URL,
|
|
273
|
+
currentPath: window.location.pathname,
|
|
274
|
+
gatewayUrl: GATEWAY_URL,
|
|
275
|
+
appName: APP_NAME,
|
|
276
|
+
appBaseUrl: APP_BASE_URL, // e.g. "http://localhost:3000"
|
|
277
|
+
connector: CONNECTOR_MYGOVID,
|
|
278
|
+
})
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 4. Citizen is onboarded → proceed normally
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The redirect chain is:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
app → profile-service/onboarding?source=<gateway-sign-in-url>
|
|
289
|
+
→ (user completes onboarding, gains "Onboarded citizen" role)
|
|
290
|
+
→ GET gateway/auth/sign-in?app=<name>&redirectUrl=<app-url>&connector=social:mygovid
|
|
291
|
+
→ Logto OIDC flow (fresh claims)
|
|
292
|
+
→ gateway/auth/callback
|
|
293
|
+
→ app (original page, now with updated roles)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
> **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.
|
|
297
|
+
|
|
298
|
+
### Types
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import type {
|
|
302
|
+
ActorType, // "user" | "m2m"
|
|
303
|
+
AuthClaims, // { roles, organizations, organization_roles, signinMethod? }
|
|
304
|
+
AuthStatus,
|
|
305
|
+
AuthUser,
|
|
306
|
+
GatewayFetchOptions, // { actorType?, organizationId? }
|
|
307
|
+
OrganizationInfo, // { id, name, description?, roles }
|
|
308
|
+
SagClientConfig,
|
|
309
|
+
SignInOptions, // { connector?, redirectUrl? }
|
|
310
|
+
UseAuthResult,
|
|
311
|
+
UseOnboardingGuardOptions,
|
|
312
|
+
UseOnboardingGuardResult,
|
|
313
|
+
UsePublicServantGuardOptions,
|
|
314
|
+
UsePublicServantGuardResult,
|
|
315
|
+
OnboardingRedirectParams,
|
|
316
|
+
WrongLoginMethodParams,
|
|
317
|
+
} from "@ogcio/sag-client"
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## React API (`@ogcio/sag-client/react`)
|
|
321
|
+
|
|
322
|
+
### Provider
|
|
323
|
+
|
|
324
|
+
Wrap your app with `SagClientProvider`:
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
import { SagClientProvider } from "@ogcio/sag-client/react"
|
|
328
|
+
|
|
329
|
+
export function Providers({ children }) {
|
|
330
|
+
return (
|
|
331
|
+
<SagClientProvider gatewayUrl="http://localhost:3333" appName="cars">
|
|
332
|
+
{children}
|
|
333
|
+
</SagClientProvider>
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Hooks
|
|
339
|
+
|
|
340
|
+
#### `useAuth`
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
import {
|
|
344
|
+
useAuth,
|
|
345
|
+
useSagClient,
|
|
346
|
+
CONNECTOR_MYGOVID,
|
|
347
|
+
} from "@ogcio/sag-client/react"
|
|
348
|
+
|
|
349
|
+
function MyComponent() {
|
|
350
|
+
const {
|
|
351
|
+
authenticated,
|
|
352
|
+
user,
|
|
353
|
+
claims, // { roles, organizations, organization_roles, signinMethod? }
|
|
354
|
+
loading,
|
|
355
|
+
logtoAvailable,
|
|
356
|
+
signIn, // accepts optional SignInOptions
|
|
357
|
+
signOut,
|
|
358
|
+
invalidateSession,
|
|
359
|
+
refresh,
|
|
360
|
+
} = useAuth()
|
|
361
|
+
|
|
362
|
+
// Sign in with a specific connector
|
|
363
|
+
const handleCitizenSignIn = () => signIn({ connector: CONNECTOR_MYGOVID })
|
|
364
|
+
|
|
365
|
+
// Direct client access (advanced)
|
|
366
|
+
const client = useSagClient()
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
#### `useGatewayFetch`
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
import { useGatewayFetch } from "@ogcio/sag-client/react"
|
|
374
|
+
|
|
375
|
+
// SWR-based data fetching
|
|
376
|
+
const { data, error, isLoading, refresh } = useGatewayFetch<Car[]>("/cars")
|
|
377
|
+
|
|
378
|
+
// Conditional fetching
|
|
379
|
+
const { data: detail } = useGatewayFetch<Car>(`/cars/${id}`, { enabled: !!id })
|
|
380
|
+
|
|
381
|
+
// M2M fetching (sends X-Request-Actor-Type: m2m header)
|
|
382
|
+
const { data: notifications } = useGatewayFetch<Notification[]>("/notifications", {
|
|
383
|
+
actorType: "m2m",
|
|
384
|
+
})
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### `useOnboardingGuard`
|
|
388
|
+
|
|
389
|
+
Encapsulates the full citizen onboarding check for React applications. Must be used inside a `SagClientProvider`. Reads `gatewayUrl` and `appName` from the provider context automatically.
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
import {
|
|
393
|
+
useOnboardingGuard,
|
|
394
|
+
useAuth,
|
|
395
|
+
CONNECTOR_MYGOVID,
|
|
396
|
+
} from "@ogcio/sag-client/react"
|
|
397
|
+
|
|
398
|
+
function ShellContent({ children }) {
|
|
399
|
+
// publicServantRoles defaults to DEFAULT_PUBLIC_SERVANT_ROLES
|
|
400
|
+
// (["Organisation Admin", "Organisation Member"])
|
|
401
|
+
const { resolved } = useOnboardingGuard({
|
|
402
|
+
profileUrl: "http://localhost:3001",
|
|
403
|
+
appBaseUrl: "http://localhost:3000",
|
|
404
|
+
connector: CONNECTOR_MYGOVID, // optional
|
|
405
|
+
// publicServantRoles: ["Custom Role"], // override for admin apps
|
|
406
|
+
// debounceMs: 30_000, // optional, default: 30000
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
const { user, signIn, signOut } = useAuth()
|
|
410
|
+
|
|
411
|
+
if (!resolved) return <Loading />
|
|
412
|
+
|
|
413
|
+
return user ? <App>{children}</App> : <SignInButton />
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Options:**
|
|
418
|
+
|
|
419
|
+
| Option | Type | Required | Default | Description |
|
|
420
|
+
|--------|------|----------|---------|-------------|
|
|
421
|
+
| `profileUrl` | `string` | Yes | - | Profile service base URL |
|
|
422
|
+
| `appBaseUrl` | `string` | Yes | - | Consuming application's base URL (for post-auth redirect) |
|
|
423
|
+
| `publicServantRoles` | `string[]` | No | `DEFAULT_PUBLIC_SERVANT_ROLES` | Organisation roles that identify a public servant |
|
|
424
|
+
| `connector` | `string` | No | `undefined` | Logto `directSignIn` connector (e.g. `"social:mygovid"`) |
|
|
425
|
+
| `debounceMs` | `number` | No | `30000` | Time window (ms) to prevent redirect loops |
|
|
426
|
+
|
|
427
|
+
**Return value:**
|
|
428
|
+
|
|
429
|
+
| Property | Type | Description |
|
|
430
|
+
|----------|------|-------------|
|
|
431
|
+
| `resolved` | `boolean` | `true` when the onboarding check has passed; `false` while loading or redirecting |
|
|
432
|
+
|
|
433
|
+
**Behaviour:**
|
|
434
|
+
|
|
435
|
+
1. User is onboarded (`isCitizenOnboarded`) → `resolved = true`
|
|
436
|
+
2. User is not a citizen → `resolved = true` (let them through)
|
|
437
|
+
3. Recent redirect within `debounceMs` → `resolved = true` (prevent loops)
|
|
438
|
+
4. Wrong sign-in method → redirect to profile service error page
|
|
439
|
+
5. Citizen not onboarded → `invalidateSession()` then redirect to profile service onboarding page
|
|
440
|
+
|
|
441
|
+
> **Important:** Do not render data-fetching children (e.g. `useGatewayFetch`) until `resolved` is `true` to prevent API requests from racing with session invalidation.
|
|
442
|
+
|
|
443
|
+
#### `usePublicServantGuard`
|
|
444
|
+
|
|
445
|
+
Access guard hook for **public-servant-facing** (admin) applications. Checks if the authenticated user is an active public servant and optionally redirects unauthorized users.
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
import { usePublicServantGuard, useAuth } from "@ogcio/sag-client/react"
|
|
449
|
+
|
|
450
|
+
function AdminShell({ children }) {
|
|
451
|
+
const { resolved, authorized, isInactive } = usePublicServantGuard({
|
|
452
|
+
// Redirect citizens to the citizen-facing app (optional)
|
|
453
|
+
unauthorizedRedirectUrl: "https://citizen-app.example.com",
|
|
454
|
+
// Redirect inactive PS to a specific page (optional)
|
|
455
|
+
inactiveRedirectUrl: "https://admin.example.com/inactive",
|
|
456
|
+
// publicServantRoles: ["Custom Admin Role"], // override defaults
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
const { user } = useAuth()
|
|
460
|
+
|
|
461
|
+
if (!resolved) return <Loading />
|
|
462
|
+
if (!authorized) return <AccessDenied />
|
|
463
|
+
return <>{children}</>
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Options:**
|
|
468
|
+
|
|
469
|
+
| Option | Type | Required | Default | Description |
|
|
470
|
+
|--------|------|----------|---------|-------------|
|
|
471
|
+
| `publicServantRoles` | `string[]` | No | `DEFAULT_PUBLIC_SERVANT_ROLES` | Organisation roles that identify an active public servant |
|
|
472
|
+
| `inactiveRedirectUrl` | `string` | No | `undefined` | URL to redirect inactive PS users to. If omitted, they pass through with `isInactive: true`. |
|
|
473
|
+
| `unauthorizedRedirectUrl` | `string` | No | `undefined` | URL to redirect non-PS users to. If omitted, they pass through with `authorized: false`. |
|
|
474
|
+
|
|
475
|
+
**Return value:**
|
|
476
|
+
|
|
477
|
+
| Property | Type | Description |
|
|
478
|
+
|----------|------|-------------|
|
|
479
|
+
| `resolved` | `boolean` | `true` once the guard check has completed |
|
|
480
|
+
| `authorized` | `boolean` | `true` when the user is an active public servant |
|
|
481
|
+
| `isInactive` | `boolean` | `true` when the user is an inactive public servant |
|
|
482
|
+
|
|
483
|
+
#### `useGatewayFetch` with organization scope
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
import { useGatewayFetch } from "@ogcio/sag-client/react"
|
|
487
|
+
|
|
488
|
+
// Fetch with an organization-scoped token
|
|
489
|
+
const { data } = useGatewayFetch<Message[]>("/messaging", {
|
|
490
|
+
organizationId: "org_xxx",
|
|
491
|
+
})
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
#### Role detection utilities
|
|
495
|
+
|
|
496
|
+
Role detection functions are also re-exported from the React entry point for convenience:
|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
import {
|
|
500
|
+
isCitizen,
|
|
501
|
+
isCitizenOnboarded,
|
|
502
|
+
isPublicServant,
|
|
503
|
+
CONNECTOR_MYGOVID,
|
|
504
|
+
ALLOWED_SIGNIN_METHODS,
|
|
505
|
+
DEFAULT_PUBLIC_SERVANT_ROLES,
|
|
506
|
+
ORG_ROLE_ADMIN,
|
|
507
|
+
ORG_ROLE_MEMBER,
|
|
508
|
+
ONBOARDING_PATH,
|
|
509
|
+
ONBOARDING_SOURCE_PARAM,
|
|
510
|
+
WRONG_LOGIN_METHOD_PATH,
|
|
511
|
+
WRONG_LOGIN_RETURN_URL_PARAM,
|
|
512
|
+
} from "@ogcio/sag-client/react"
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Package Structure
|
|
516
|
+
|
|
517
|
+
```
|
|
518
|
+
src/
|
|
519
|
+
index.ts # Core entry point
|
|
520
|
+
client.ts # SagClient class
|
|
521
|
+
auth.ts # Standalone auth functions (signIn, signOut, invalidateSession, etc.)
|
|
522
|
+
fetcher.ts # Gateway fetcher factory
|
|
523
|
+
roles.ts # Role detection utilities (isCitizen, isPublicServant, etc.)
|
|
524
|
+
onboarding.ts # Onboarding & wrong-login-method URL builders
|
|
525
|
+
types.ts # All type definitions
|
|
526
|
+
react/
|
|
527
|
+
index.ts # React entry point (re-exports roles & onboarding for convenience)
|
|
528
|
+
provider.tsx # SagClientProvider + useSagClient
|
|
529
|
+
use-auth.ts # useAuth hook (with claims, connector-aware signIn, invalidateSession)
|
|
530
|
+
use-gateway-fetch.ts # useGatewayFetch hook
|
|
531
|
+
use-onboarding-guard.ts # useOnboardingGuard hook (citizen onboarding guard)
|
|
532
|
+
use-public-servant-guard.ts # usePublicServantGuard hook (admin app access guard)
|
|
533
|
+
```
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone authentication helpers for the Secure API Gateway.
|
|
3
|
+
*
|
|
4
|
+
* These are pure functions — no React, no framework dependency.
|
|
5
|
+
* They accept `gatewayUrl` (and optionally `appName`) so they
|
|
6
|
+
* can be called without a SagClient instance.
|
|
7
|
+
*/
|
|
8
|
+
import type { AuthStatus, OrganizationInfo, SignInOptions } from "./types";
|
|
9
|
+
/**
|
|
10
|
+
* Check if the user is authenticated against the gateway.
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkAuth(gatewayUrl: string): Promise<AuthStatus>;
|
|
13
|
+
/**
|
|
14
|
+
* Check if the authentication service (Logto) is reachable.
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkHealth(gatewayUrl: string): Promise<{
|
|
17
|
+
available: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Invalidate the current server session so the next sign-in
|
|
21
|
+
* fetches fresh claims. Used after onboarding completes (the user
|
|
22
|
+
* gains new roles that must appear in the next ID token).
|
|
23
|
+
*/
|
|
24
|
+
export declare function invalidateSession(gatewayUrl: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Fetch structured organization data for the authenticated user.
|
|
27
|
+
* Requires an active session. Returns an empty array if not authenticated.
|
|
28
|
+
*/
|
|
29
|
+
export declare function fetchOrganizations(gatewayUrl: string): Promise<OrganizationInfo[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Set the user's currently selected organization on the gateway.
|
|
32
|
+
* Persists as a signed cookie.
|
|
33
|
+
*/
|
|
34
|
+
export declare function selectOrganization(gatewayUrl: string, organizationId: string): Promise<boolean>;
|
|
35
|
+
/**
|
|
36
|
+
* Get the user's currently selected organization from the gateway.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getSelectedOrganization(gatewayUrl: string): Promise<string | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Clear the user's organization selection on the gateway.
|
|
41
|
+
*/
|
|
42
|
+
export declare function clearSelectedOrganization(gatewayUrl: string): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Redirect the user to the gateway sign-in endpoint.
|
|
45
|
+
*
|
|
46
|
+
* @param gatewayUrl - Secure API Gateway base URL.
|
|
47
|
+
* @param app - Application name registered in the gateway.
|
|
48
|
+
* @param options - Optional sign-in options (connector, redirectUrl).
|
|
49
|
+
*/
|
|
50
|
+
export declare function signIn(gatewayUrl: string, app: string, options?: SignInOptions): void;
|
|
51
|
+
/**
|
|
52
|
+
* Redirect the user to the gateway sign-out endpoint.
|
|
53
|
+
*/
|
|
54
|
+
export declare function signOut(gatewayUrl: string, app: string): void;
|
|
55
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE1E;;GAEG;AACH,wBAAsB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAUvE;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CAUjC;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKzE;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAW7B;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,OAAO,CAAC,CAYlB;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWxB;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CAUlB;AAiBD;;;;;;GAMG;AACH,wBAAgB,MAAM,CACpB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,aAAa,GACtB,IAAI,CAKN;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAE7D"}
|