@b1-road/react 0.1.0-alpha.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +300 -0
  3. package/dist/__tests__/a11y.test.d.ts +1 -0
  4. package/dist/__tests__/contract-replay.test.d.ts +1 -0
  5. package/dist/__tests__/create-role-dialog.test.d.ts +1 -0
  6. package/dist/__tests__/form-errors.test.d.ts +1 -0
  7. package/dist/__tests__/pending-invitations.test.d.ts +1 -0
  8. package/dist/__tests__/setup.d.ts +0 -0
  9. package/dist/api/client.d.ts +253 -0
  10. package/dist/api/client.test.d.ts +1 -0
  11. package/dist/api/cookie-mode.test.d.ts +1 -0
  12. package/dist/api/errors.d.ts +107 -0
  13. package/dist/api/errors.test.d.ts +1 -0
  14. package/dist/api/hooks.d.ts +126 -0
  15. package/dist/api/hooks.test.d.ts +1 -0
  16. package/dist/api/mock-client.d.ts +121 -0
  17. package/dist/api/mock-client.test.d.ts +1 -0
  18. package/dist/api/types.d.ts +7 -0
  19. package/dist/appearance/appearance.d.ts +19 -0
  20. package/dist/components/BusinessUnitSwitcher.d.ts +15 -0
  21. package/dist/components/BusinessUnitsMgmt.d.ts +35 -0
  22. package/dist/components/business-units/BusinessUnitDetail.d.ts +6 -0
  23. package/dist/components/business-units/BusinessUnitList.d.ts +5 -0
  24. package/dist/components/business-units/BusinessUnitRow.d.ts +7 -0
  25. package/dist/components/business-units/BusinessUnitSettings.d.ts +5 -0
  26. package/dist/components/business-units/BusinessUnitsTab.d.ts +5 -0
  27. package/dist/components/business-units/CreateBusinessUnitForm.d.ts +6 -0
  28. package/dist/components/business-units/MembersList.d.ts +5 -0
  29. package/dist/components/business-units/PendingInvitations.d.ts +15 -0
  30. package/dist/components/invitations/InvitationsList.d.ts +7 -0
  31. package/dist/components/invitations/InvitationsTab.d.ts +5 -0
  32. package/dist/components/invitations/InviteForm.d.ts +7 -0
  33. package/dist/components/roles/BUSelector.d.ts +8 -0
  34. package/dist/components/roles/CreateRoleDialog.d.ts +8 -0
  35. package/dist/components/roles/PermissionPicker.d.ts +10 -0
  36. package/dist/components/roles/RoleEditor.d.ts +7 -0
  37. package/dist/components/roles/RoleRow.d.ts +7 -0
  38. package/dist/components/roles/RolesList.d.ts +7 -0
  39. package/dist/components/roles/RolesTab.d.ts +1 -0
  40. package/dist/components/shared/Avatar.d.ts +7 -0
  41. package/dist/components/shared/EmptyState.d.ts +10 -0
  42. package/dist/components/shared/LoadMoreFooter.d.ts +18 -0
  43. package/dist/i18n/context.d.ts +21 -0
  44. package/dist/i18n/en.d.ts +2 -0
  45. package/dist/i18n/pt-BR.d.ts +2 -0
  46. package/dist/i18n/types.d.ts +227 -0
  47. package/dist/index.cjs +56 -0
  48. package/dist/index.cjs.map +1 -0
  49. package/dist/index.d.ts +25 -0
  50. package/dist/index.js +16566 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/lib/cn.d.ts +2 -0
  53. package/dist/lib/use-form-errors.d.ts +34 -0
  54. package/dist/provider/RoadProvider.d.ts +93 -0
  55. package/dist/provider/context.d.ts +12 -0
  56. package/dist/provider/cookie-mode-integration.test.d.ts +1 -0
  57. package/dist/provider/current-business-unit.d.ts +37 -0
  58. package/dist/provider/current-business-unit.test.d.ts +1 -0
  59. package/dist/provider/strict-mode-checks.test.d.ts +1 -0
  60. package/dist/style.css +1 -0
  61. package/dist/ui/alert-dialog.d.ts +8 -0
  62. package/dist/ui/badge.d.ts +9 -0
  63. package/dist/ui/button.d.ts +11 -0
  64. package/dist/ui/checkbox.d.ts +2 -0
  65. package/dist/ui/dialog.d.ts +17 -0
  66. package/dist/ui/dropdown-menu.d.ts +10 -0
  67. package/dist/ui/field.d.ts +26 -0
  68. package/dist/ui/input.d.ts +4 -0
  69. package/dist/ui/label.d.ts +2 -0
  70. package/dist/ui/select.d.ts +6 -0
  71. package/dist/ui/skeleton.d.ts +2 -0
  72. package/dist/ui/tabs.d.ts +5 -0
  73. package/dist/ui/textarea.d.ts +2 -0
  74. package/dist/ui/tooltip.d.ts +5 -0
  75. package/package.json +126 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 B1 Produtos Digitais Ltda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # @b1-road/react
2
+
3
+ The official React toolkit for integrating with Road — components, hooks,
4
+ and helpers for embedding Road IAM into platform apps.
5
+
6
+ See [`docs/plans/done/03-road-react-package-plan.md`](../../../docs/plans/done/03-road-react-package-plan.md)
7
+ for the predecessor plan (public API surface, hooks taxonomy, packaging)
8
+ and [`docs/plans/10-react-cookie-mode-spec.md`](../../../docs/plans/10-react-cookie-mode-spec.md)
9
+ for the cookie-mode default.
10
+
11
+ ## Status
12
+
13
+ First public alpha. Cookie mode is the default. Bearer mode remains
14
+ available behind `authMode="bearer"` for headless / mobile / partner
15
+ integrations.
16
+
17
+ If you're coming from a bearer-first setup, opt in explicitly by adding
18
+ one prop (`authMode="bearer"`). See [Coming from a bearer-first setup](#coming-from-a-bearer-first-setup)
19
+ below.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm install @b1-road/react@alpha
25
+ ```
26
+
27
+ > Published under the `alpha` dist-tag while the Road API contract is in alpha —
28
+ > install with the explicit `@alpha` tag (there is no `latest` release yet).
29
+
30
+ ## Quick start (cookie mode — recommended)
31
+
32
+ You are probably here because you're behind a Laravel or NestJS BFF
33
+ (`b1-road/laravel` or `@b1-road/nestjs`) that proxies your app to the
34
+ Road API. The BFF holds the Auth Server JWT server-side; the browser
35
+ ships a session cookie. The React SDK never sees a token.
36
+
37
+ ```tsx
38
+ import {
39
+ RoadProvider,
40
+ BusinessUnitSwitcher,
41
+ BusinessUnitsMgmt,
42
+ } from "@b1-road/react";
43
+ import "@b1-road/react/style.css";
44
+
45
+ export function App() {
46
+ return (
47
+ <RoadProvider
48
+ apiBaseUrl="/road-api"
49
+ onUnauthenticated={() => window.location.assign("/auth/road/login")}
50
+ >
51
+ <BusinessUnitSwitcher />
52
+ <BusinessUnitsMgmt />
53
+ </RoadProvider>
54
+ );
55
+ }
56
+ ```
57
+
58
+ That's the whole integration. The SDK:
59
+
60
+ - Sends requests with `credentials: 'include'`, no `Authorization`
61
+ header. The BFF attaches the bearer server-side.
62
+ - Echoes the `XSRF-TOKEN` cookie into `X-XSRF-TOKEN` on every mutation
63
+ (Laravel's convention; the NestJS BFF adopts the same names).
64
+ - Calls `onUnauthenticated` on terminal 401 with a 1s debounce so
65
+ parallel React Query refetches trigger one redirect, not a stampede.
66
+
67
+ ### Required: `onUnauthenticated`
68
+
69
+ Cookie mode requires `onUnauthenticated`. Without it the SDK logs a
70
+ one-time `console.warn` and 401s become silent. The handler typically
71
+ redirects to the BFF's login URL.
72
+
73
+ ### Optional cookie-mode knobs
74
+
75
+ ```tsx
76
+ <RoadProvider
77
+ apiBaseUrl="/road-api"
78
+ onUnauthenticated={...}
79
+ csrfCookieName="XSRF-TOKEN" // default — matches Laravel + NestJS BFFs
80
+ csrfHeaderName="X-XSRF-TOKEN" // default
81
+ withCredentials={false} // set true only for cross-origin BFFs
82
+ />
83
+ ```
84
+
85
+ Defaults are tuned for the same-origin BFF deployment that the Laravel
86
+ and NestJS plans ship.
87
+
88
+ ## Bearer mode (advanced / opt-out)
89
+
90
+ For headless apps, React Native, partner integrations, or any
91
+ deployment where the host actually holds the JWT in JS:
92
+
93
+ ```tsx
94
+ <RoadProvider
95
+ apiBaseUrl="https://api.road.b1.app"
96
+ authMode="bearer"
97
+ jwt={() => getToken()}
98
+ >
99
+ ...
100
+ </RoadProvider>
101
+ ```
102
+
103
+ `jwt` accepts either a static string or a getter (`() => string |
104
+ Promise<string>`). **Prefer the getter** — Auth Server access tokens
105
+ are short-lived (15–60 minutes); the getter re-resolves per request so
106
+ host-side rotation works without re-mounting the provider.
107
+
108
+ The trade-offs vs cookie mode, honestly stated:
109
+
110
+ - **XSS surface.** A token in JS reachable from JS-evaluated XSS is
111
+ fundamentally weaker than a `HttpOnly` session cookie. Cookie mode
112
+ removes this surface entirely.
113
+ - **Refresh complexity.** Bearer-mode integrators own token rotation;
114
+ cookie-mode BFFs do it server-side.
115
+ - **IETF BCP.** The IETF *OAuth 2.0 for Browser-Based Applications*
116
+ BCP (draft -26) ranks BFF first; PKCE-in-browser is the fallback.
117
+
118
+ ## Coming from a bearer-first setup
119
+
120
+ If your app holds the JWT in JS and looks like:
121
+
122
+ ```tsx
123
+ <RoadProvider apiBaseUrl="https://api.road.b1.app" jwt={() => getToken()}>
124
+ ```
125
+
126
+ …opt into bearer mode explicitly by adding one prop:
127
+
128
+ ```tsx
129
+ <RoadProvider
130
+ apiBaseUrl="https://api.road.b1.app"
131
+ authMode="bearer"
132
+ jwt={() => getToken()}
133
+ >
134
+ ```
135
+
136
+ The provider throws a loud, actionable error at boot if `authMode`
137
+ is omitted but `jwt` is provided — so the failure mode is "won't
138
+ mount in dev" rather than "401 in production." The error message
139
+ contains the diff above.
140
+
141
+ ## Why two modes
142
+
143
+ Cookie mode and bearer mode exist because no single deployment shape
144
+ fits every Road consumer. Cookie mode covers same-origin BFFs (Laravel
145
+ + Inertia + React, NestJS BFF + React SPA); bearer mode covers
146
+ headless / mobile / partner integrations where the host genuinely owns
147
+ the JWT.
148
+
149
+ This is the same pattern Auth0 ships (`@auth0/auth0-react` bearer-only
150
+ plus `@auth0/nextjs-auth0` cookie BFF), Clerk ships (cookie-based
151
+ session with a bearer fallback), and WorkOS AuthKit ships (cookie-only
152
+ authkit-react). Plan
153
+ [10-react-cookie-mode-spec.md](../../../docs/plans/10-react-cookie-mode-spec.md)
154
+ has the detailed rationale.
155
+
156
+ ## Permission-gated UI
157
+
158
+ Widgets gate themselves. For your own UI, `useCan(buId)` returns a
159
+ callable that checks the current user's effective permissions for that
160
+ business unit (read from the server, cached in React Query):
161
+
162
+ ```tsx
163
+ import { useCan } from "@b1-road/react";
164
+
165
+ function InviteButton({ buId }: { buId: string }) {
166
+ const can = useCan(buId); // omit buId to read the BU from <BusinessUnitSwitcher />
167
+ return can("manage:Member") ? <button>Invite member</button> : null;
168
+ }
169
+ ```
170
+
171
+ `"manage:Member"` is compile-time-checked against Road's permission
172
+ algebra (`RoadPermission` — `${action}:${Subject}` plus `"*"`);
173
+ platform-specific permission strings pass through too. For several
174
+ checks in one render, `useCanMany([...])` resolves them in a single
175
+ batch. This is a UI hint, not a security boundary — see
176
+ [Security model](#security-model).
177
+
178
+ ## Errors
179
+
180
+ Every failed call rejects with a typed subclass of `RoadApiError`
181
+ carrying the ids you need to find it in Road's logs:
182
+
183
+ ```tsx
184
+ import { RoadForbiddenError, RoadValidationError } from "@b1-road/react";
185
+
186
+ try {
187
+ await client.createRole(buId, { name, permissions });
188
+ } catch (err) {
189
+ if (err instanceof RoadValidationError) {
190
+ err.fieldErrors; // { name: ["already taken"] }
191
+ } else if (err instanceof RoadForbiddenError) {
192
+ err.requestId; // correlates to the Road API log line
193
+ err.traceId; // W3C trace id, when the API emits one
194
+ }
195
+ }
196
+ ```
197
+
198
+ React errors are deliberately thinner than the server SDKs' — the full
199
+ authorization `DecisionTrace` is surfaced **server-side**, and appended
200
+ to the 403 response body in non-prod via the debug header (see
201
+ [Security model](#security-model)). It is not rehydrated onto the
202
+ browser error object; the browser gets the correlation ids and asks the
203
+ server for the rest.
204
+
205
+ ## Testing
206
+
207
+ The SDK ships its own in-memory client — no Auth Server, no mock-fetch
208
+ boilerplate, no fake JWTs. Assemble state with the fluent builder and
209
+ hand it to the provider's `client` override:
210
+
211
+ ```tsx
212
+ import { render, screen } from "@testing-library/react";
213
+ import {
214
+ RoadProvider,
215
+ BusinessUnitsMgmt,
216
+ mockClientBuilder,
217
+ } from "@b1-road/react";
218
+
219
+ const client = mockClientBuilder()
220
+ .withBusinessUnit({ name: "Acme", role: "Owner" }) // current user is Owner (wildcard)
221
+ .withMember("Acme", { name: "Alex", email: "alex@x.com", role: "Admin" })
222
+ .withInvitation("Acme", { email: "pending@x.com", roleName: "Member" })
223
+ .build();
224
+
225
+ render(
226
+ <RoadProvider client={client}>
227
+ <BusinessUnitsMgmt />
228
+ </RoadProvider>,
229
+ );
230
+
231
+ expect(await screen.findByText("Acme")).toBeInTheDocument();
232
+ ```
233
+
234
+ `createMockClient({ empty, latency, failWith })` is the one-liner for
235
+ happy-path / empty / slow-network cases; `mockClientBuilder()` is for
236
+ curated state. Drive error and unauthenticated paths with a scenario:
237
+
238
+ ```tsx
239
+ const client = mockClientBuilder()
240
+ .withScenario("auth-error") // also: "network-error" | "rate-limit" | "server-error"
241
+ .withCookieMode({ onUnauthenticated: redirectSpy })
242
+ .build();
243
+ ```
244
+
245
+ **Scope.** The in-memory client exercises the SDK's hooks, widgets, and
246
+ React Query wiring — not its HTTP transport. It has no `fetch`, so it
247
+ can't assert cookie-mode wire behavior (`credentials: 'include'`, the
248
+ `XSRF-TOKEN` echo, the omitted `Authorization` header). For those, stub
249
+ `globalThis.fetch` against `HttpRoadClient` directly.
250
+
251
+ ## Security model
252
+
253
+ **Authorization is server-side authoritative.** Every data call goes
254
+ through the Road API, which validates the JWT signature against the
255
+ configured Auth Server's JWKS and rejects forged, expired, or revoked
256
+ tokens. The toolkit's `useCan`/`useCanMany` hooks are a UI hint — they
257
+ read the server's effective-permissions response cached in React Query.
258
+ A user who tampers with their session client-side gets a 401 on the
259
+ first data call, not a privilege escalation.
260
+
261
+ **Identity in cookie mode.** `getCurrentUser()` calls `GET /me/profile`
262
+ on the Road API. The API resolves the user from the bearer the BFF
263
+ attaches; the SDK never decodes claims locally.
264
+
265
+ **Identity in bearer mode.** `getCurrentUser()` decodes `sub`,
266
+ `email`, `name`, `picture` from the JWT payload without signature
267
+ verification — a cosmetic-only trust model (a tampered token still
268
+ gets rejected on the next data call). Bearer-mode integrators who
269
+ need server-authoritative identity can call the API's `/me/profile`
270
+ endpoint directly.
271
+
272
+ ## Theming
273
+
274
+ All widget styles live under a `.road-ui` scope — tokens never leak into the host page's `:root`. Override via `appearance.variables`; the supported keys map to internal CSS variables:
275
+
276
+ | Variable | CSS var |
277
+ | ------------------------ | ------------------------ |
278
+ | `colorPrimary` | `--primary` |
279
+ | `colorPrimaryForeground` | `--primary-foreground` |
280
+ | `colorBackground` | `--background` |
281
+ | `colorForeground` | `--foreground` |
282
+ | `colorMuted` | `--muted` |
283
+ | `colorAccent` | `--accent` |
284
+ | `colorDanger` | `--destructive` |
285
+ | `colorBorder` | `--border` |
286
+ | `borderRadius` | `--radius` |
287
+ | `fontFamily` | `--font-family` |
288
+
289
+ Accepts any valid CSS color (hex, rgb, hsl, oklch) and CSS lengths.
290
+
291
+ ## Local development
292
+
293
+ ```bash
294
+ npm install
295
+ npm run dev # opens the playground at http://localhost:5174
296
+ npm run build # library build → dist/
297
+ npm run typecheck
298
+ ```
299
+
300
+ The playground (`playground/`) mounts the widget against mocked data — no Road API required.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
File without changes
@@ -0,0 +1,253 @@
1
+ import { BusinessUnitDetail, CreateBusinessUnitInput, CreateInvitationInput, CreateRoleInput, CurrentUser, Invitation, Member, MyBusinessUnits, MyPermissions, PaginatedList, PaginationInput, Permission, Role, UpdateBusinessUnitInput, UpdateRoleInput } from './types';
2
+ import { RoadApiError } from './errors';
3
+ export interface RoadClient {
4
+ getCurrentUser(): Promise<CurrentUser>;
5
+ getMyBusinessUnits(): Promise<MyBusinessUnits>;
6
+ getMyPermissions(): Promise<MyPermissions>;
7
+ getBusinessUnit(buId: string): Promise<BusinessUnitDetail>;
8
+ createBusinessUnit(input: CreateBusinessUnitInput): Promise<BusinessUnitDetail>;
9
+ updateBusinessUnit(buId: string, input: UpdateBusinessUnitInput): Promise<BusinessUnitDetail>;
10
+ listMembers(buId: string, pagination?: PaginationInput): Promise<PaginatedList<Member>>;
11
+ suspendMember(buId: string, memberId: string): Promise<void>;
12
+ reinstateMember(buId: string, memberId: string): Promise<void>;
13
+ removeMember(buId: string, memberId: string): Promise<void>;
14
+ listInvitations(buId: string, pagination?: PaginationInput): Promise<PaginatedList<Invitation>>;
15
+ createInvitation(buId: string, input: CreateInvitationInput): Promise<Invitation>;
16
+ cancelInvitation(buId: string, invitationId: string): Promise<void>;
17
+ resendInvitation(buId: string, invitationId: string): Promise<void>;
18
+ acceptInvitation(invitationId: string): Promise<void>;
19
+ rejectInvitation(invitationId: string): Promise<void>;
20
+ listRoles(buId: string, pagination?: PaginationInput): Promise<PaginatedList<Role>>;
21
+ getRole(buId: string, roleId: string): Promise<Role>;
22
+ createRole(buId: string, input: CreateRoleInput): Promise<Role>;
23
+ updateRole(buId: string, roleId: string, input: UpdateRoleInput): Promise<Role>;
24
+ deleteRole(buId: string, roleId: string): Promise<void>;
25
+ listPermissions(buId: string): Promise<Permission[]>;
26
+ /**
27
+ * Authoritative batch permission check — calls the IAM engine and
28
+ * returns a record from permission code → allowed boolean. Honors
29
+ * the engine's full evaluation (manage → CRUD expansion, wildcards,
30
+ * scope inheritance) — strictly more accurate than the string-match
31
+ * useCan does over the cached useMyPermissions map.
32
+ */
33
+ authorizeBatch(buId: string, permissions: readonly string[]): Promise<Record<string, boolean>>;
34
+ }
35
+ /**
36
+ * JWT-source contract. A plain string is accepted for backward-compat,
37
+ * but integrators should pass a getter so token rotation in the host's
38
+ * auth state is picked up on every request without forcing a re-render
39
+ * of <RoadProvider>.
40
+ */
41
+ export type JwtSource = string | (() => string | Promise<string> | null | undefined);
42
+ /**
43
+ * How the SDK authenticates against the Road API.
44
+ *
45
+ * - `'cookie'` — true BFF mode. The browser holds an opaque session
46
+ * cookie; the BFF (Laravel / NestJS) proxies requests to Road and
47
+ * attaches the Auth Server JWT server-side. The SDK sends
48
+ * `credentials: 'include'` and **no** `Authorization` header. On
49
+ * non-GETs the SDK echoes a CSRF token from a cookie into the
50
+ * matching header. This is the default and the recommended path per
51
+ * the IETF OAuth-for-Browser-Apps BCP.
52
+ * - `'bearer'` — the host holds the JWT in JS (legacy SPAs, mobile
53
+ * webviews, headless / partner integrations). The SDK reads it from
54
+ * the `jwt` source and sends `Authorization: Bearer …` on every
55
+ * request. No cookie semantics.
56
+ *
57
+ * Picking the mode is a deployment-shape decision, not a runtime
58
+ * switch — flipping it mid-app changes nothing useful and breaks the
59
+ * server-side auth pipeline.
60
+ */
61
+ export type RoadAuthMode = "cookie" | "bearer";
62
+ export interface RetryConfig {
63
+ /** Max attempts including the first try. Default 3. */
64
+ maxAttempts?: number;
65
+ /** Base delay in ms for the exponential backoff. Default 250. */
66
+ baseDelay?: number;
67
+ }
68
+ export interface TelemetryHooks {
69
+ /** Fires after a request completes successfully. */
70
+ onRequest?: (event: TelemetryRequestEvent) => void;
71
+ /** Fires after a request fails (after retries are exhausted). */
72
+ onError?: (error: RoadApiError, event: TelemetryRequestEvent) => void;
73
+ }
74
+ export interface TelemetryRequestEvent {
75
+ method: string;
76
+ path: string;
77
+ status: number;
78
+ durationMs: number;
79
+ /** From the Road response's `x-request-id` header; undefined when the call never reached Road. */
80
+ requestId?: string;
81
+ /** From the Road response's `traceparent` header; undefined when absent or the call never reached Road. */
82
+ traceId?: string;
83
+ /** Total attempts made before resolving (1 when no retries occurred). */
84
+ attempts: number;
85
+ }
86
+ export interface HttpClientConfig {
87
+ apiBaseUrl: string;
88
+ /**
89
+ * Transport shape. Defaults to `'cookie'` — the BFF holds the JWT and
90
+ * the browser ships a session cookie. Pass `'bearer'` only when the
91
+ * host actually holds the JWT (mobile/headless/partner integrations).
92
+ */
93
+ authMode?: RoadAuthMode;
94
+ /** Required iff `authMode === 'bearer'`. Ignored in cookie mode. */
95
+ jwt?: JwtSource;
96
+ /**
97
+ * Fired once per terminal 401 with a 1s debounce. In cookie mode the
98
+ * BFF owns refresh; the SDK can't recover and the handler should
99
+ * redirect to the BFF's login URL. In bearer mode this fires after the
100
+ * existing rotation attempt (if any) also fails.
101
+ */
102
+ onUnauthenticated?: () => void;
103
+ /**
104
+ * Cookie name the SDK reads to source a CSRF token on cookie-mode
105
+ * mutations. Default: `'XSRF-TOKEN'` (Laravel's convention; the
106
+ * NestJS BFF adopts the same).
107
+ */
108
+ csrfCookieName?: string;
109
+ /**
110
+ * Header name the SDK writes the CSRF token into on cookie-mode
111
+ * mutations. Default: `'X-XSRF-TOKEN'`.
112
+ */
113
+ csrfHeaderName?: string;
114
+ /**
115
+ * Cross-origin opt-in for cookie mode. Default `false` — same-origin
116
+ * is the recommended deployment. When `true` the BFF must respond
117
+ * with `Access-Control-Allow-Credentials: true` and matching CORS.
118
+ * Ignored in bearer mode.
119
+ */
120
+ withCredentials?: boolean;
121
+ /** Per-call retry policy for idempotent (GET) + idempotency-keyed requests. */
122
+ retry?: RetryConfig;
123
+ /** Observability hooks for the host app. */
124
+ telemetry?: TelemetryHooks;
125
+ }
126
+ export declare class HttpRoadClient implements RoadClient {
127
+ private readonly config;
128
+ /**
129
+ * buId → iamScopeId cache. Populated lazily by resolveScopeId(); the
130
+ * IAM role / permission endpoints are scope-keyed, but the SDK exposes
131
+ * BU-keyed APIs to integrators — this map bridges the two without
132
+ * forcing every call site to fetch the BU detail first.
133
+ */
134
+ private readonly scopeIdByBuId;
135
+ /**
136
+ * Timestamp of the last terminal-401 callback. Used to debounce
137
+ * `onUnauthenticated` so parallel in-flight requests that all 401 at
138
+ * once trigger a single redirect rather than a stampede.
139
+ */
140
+ private lastUnauthAt;
141
+ constructor(config: HttpClientConfig);
142
+ /**
143
+ * Fire the integrator's onUnauthenticated handler at most once per
144
+ * 1 second. The window is intentionally short — long enough to absorb
145
+ * parallel request bursts, short enough that the next manual user
146
+ * action sees a fresh callback if needed. The handler runs inside a
147
+ * try/catch so a broken integrator callback can't break the SDK.
148
+ */
149
+ private notifyUnauthenticated;
150
+ /**
151
+ * Resolve the JWT for this request. Supports the legacy `string`
152
+ * form for backward-compat, plus the recommended getter form so the
153
+ * host can rotate tokens without re-mounting <RoadProvider>.
154
+ */
155
+ private resolveJwt;
156
+ private request;
157
+ private executeRequest;
158
+ /** Fetch & unwrap a `{ data: T }` envelope. */
159
+ private fetchData;
160
+ /**
161
+ * Fetch a paginated list from a `{ data: T[], meta: { pagination } }`
162
+ * response. The RequestIdInterceptor on the API side moves the
163
+ * controller's top-level `pagination` field into `meta` so every
164
+ * paginated endpoint converges on a single response shape — this
165
+ * helper centralizes the unwrap.
166
+ *
167
+ * Falls back to a single-page response (cursor=null, hasMore=false)
168
+ * when the response has no pagination metadata, so callers that hit
169
+ * legacy endpoints still get a valid `PaginatedList<T>`.
170
+ */
171
+ private fetchPaginated;
172
+ /** Resolve buId → iamScopeId, fetching the BU detail once and caching. */
173
+ private resolveScopeId;
174
+ /**
175
+ * Cookie mode calls Road's canonical `GET /me/profile` (the single
176
+ * server-authoritative profile endpoint). Bearer mode instead decodes the
177
+ * profile from the JWT's payload claims — the Auth Server's JWT already
178
+ * carries sub / email / name / picture, so we read them client-side and
179
+ * avoid an unnecessary round-trip. The id is the JWT `sub` (the stable
180
+ * Auth Server user id, which matches Road's user references everywhere).
181
+ *
182
+ * **Trust model — read carefully (bearer mode).** This path does NOT verify
183
+ * the JWT signature. The claims are decoded with `atob` and returned as-is.
184
+ * A tampered token can produce a `CurrentUser` with arbitrary
185
+ * `name`/`email`/`avatarUrl` — but cannot perform any action against Road,
186
+ * because the API validates the signature on every data call via the Auth
187
+ * Server's JWKS endpoint and rejects forged tokens with 401. The risk is
188
+ * purely cosmetic (the switcher / member rows might display a wrong identity
189
+ * label).
190
+ *
191
+ * If your security review requires server-authoritative identity in bearer
192
+ * mode too, call `GET /me/profile` directly instead of this method.
193
+ *
194
+ * See the SDK README "Security model" section for the full discussion.
195
+ */
196
+ getCurrentUser(): Promise<CurrentUser>;
197
+ /** GET /me/business-units — server wraps in `{data}` like every other endpoint. */
198
+ getMyBusinessUnits(): Promise<MyBusinessUnits>;
199
+ /**
200
+ * Effective permissions across every membership the user has. Issues a
201
+ * single bulk request to `GET /iam/authorization/me/permissions?scopes=`
202
+ * — the API returns `{ [scopeId]: PermissionTuple[] }` which we flatten
203
+ * into the SDK's `Record<buId, string[]>` shape that useCan /
204
+ * useMyPermissions consume.
205
+ *
206
+ * The mutator already server-side-expands `manage` into the CRUD set,
207
+ * so each BU's list is complete. Wildcards (`*:*`) collapse to `"*"`
208
+ * for compatibility with useCan's wildcard-short-circuit.
209
+ *
210
+ * Resolving every BU's iamScopeId still requires a getBusinessUnit
211
+ * fetch per BU (cached after first call), but that runs in parallel —
212
+ * total round-trips: N parallel BU details + 1 batched permissions.
213
+ * The pre-bulk implementation needed 2N sequential calls.
214
+ */
215
+ getMyPermissions(): Promise<MyPermissions>;
216
+ getBusinessUnit(buId: string): Promise<BusinessUnitDetail>;
217
+ createBusinessUnit(input: CreateBusinessUnitInput): Promise<BusinessUnitDetail>;
218
+ updateBusinessUnit(buId: string, input: UpdateBusinessUnitInput): Promise<BusinessUnitDetail>;
219
+ /**
220
+ * GET .../members — cursor-paginated. Returns `{ items, pagination }` so
221
+ * callers can drive infinite scroll. The API ships paginated lists as
222
+ * `{ data: Member[], meta: { pagination: Pagination, … } }` (the
223
+ * RequestIdInterceptor absorbs the controller's top-level `pagination`
224
+ * into `meta`). We normalize that into the SDK's `PaginatedList<T>`.
225
+ */
226
+ listMembers(buId: string, pagination?: PaginationInput): Promise<PaginatedList<Member>>;
227
+ suspendMember(buId: string, memberId: string): Promise<void>;
228
+ reinstateMember(buId: string, memberId: string): Promise<void>;
229
+ removeMember(buId: string, memberId: string): Promise<void>;
230
+ listInvitations(buId: string, pagination?: PaginationInput): Promise<PaginatedList<Invitation>>;
231
+ createInvitation(buId: string, input: CreateInvitationInput): Promise<Invitation>;
232
+ cancelInvitation(buId: string, invitationId: string): Promise<void>;
233
+ resendInvitation(buId: string, invitationId: string): Promise<void>;
234
+ acceptInvitation(invitationId: string): Promise<void>;
235
+ rejectInvitation(invitationId: string): Promise<void>;
236
+ listRoles(buId: string, pagination?: PaginationInput): Promise<PaginatedList<Role>>;
237
+ getRole(buId: string, roleId: string): Promise<Role>;
238
+ createRole(buId: string, input: CreateRoleInput): Promise<Role>;
239
+ updateRole(buId: string, roleId: string, input: UpdateRoleInput): Promise<Role>;
240
+ deleteRole(buId: string, roleId: string): Promise<void>;
241
+ listPermissions(buId: string): Promise<Permission[]>;
242
+ authorizeBatch(buId: string, permissions: readonly string[]): Promise<Record<string, boolean>>;
243
+ }
244
+ /**
245
+ * Decode a JWT's payload claims without signature verification — used only
246
+ * to surface the user's identity claims to the widget UI. The actual auth
247
+ * decisions are made server-side; a tampered token will be rejected by
248
+ * Road's API on any data call.
249
+ *
250
+ * Uses `atob` because the React SDK is browser-only. A node-targeted SDK
251
+ * would swap this for a Buffer-based implementation.
252
+ */
253
+ export declare function decodeJwtPayload(jwt: string | undefined): Record<string, unknown> | null;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};