@ulabase/kit-ng 0.12.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 ADDED
@@ -0,0 +1,238 @@
1
+ # @ulabase/kit-ng
2
+
3
+ Wraps [`@ulabase/kit`](https://www.npmjs.com/package/@ulabase/kit) in Angular services with signals, route guards, and an HTTP interceptor — authentication in `RhAuthService`, and [payments](#rhpaymentsservice) in `RhPaymentsService`.
4
+
5
+ Pairs with [Ulabase](https://cloud.restheart.com), which gives you a production-ready backend — MongoDB, REST API, authentication, signup/signin, all managed.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @ulabase/kit-ng @ulabase/kit
11
+ ```
12
+
13
+ > **v0.3.0+ requires RESTHeart 9.6.0 or later.** Bearer-mode `activate()`, `resetPassword()`, and `switchTeam()` rely on the `delivery=body` query parameter, introduced in RESTHeart 9.6.0. Against an older server the request still succeeds, but the kit won't be able to capture the bearer token from the response — you'll need to log in again to get a token. Cookie mode is unaffected.
14
+
15
+ ## Setup
16
+
17
+ In `app.config.ts`:
18
+
19
+ ```typescript
20
+ import { provideRhAuth } from '@ulabase/kit-ng';
21
+
22
+ export const appConfig: ApplicationConfig = {
23
+ providers: [
24
+ provideRhAuth({ apiBaseUrl: environment.apiUrl }),
25
+ ],
26
+ };
27
+ ```
28
+
29
+ That one call registers `RhAuthService`, adds the HTTP interceptor (attaches the Bearer token, clears session on 401), and sets up the DI config.
30
+
31
+ ## How sessions work
32
+
33
+ The kit supports **two authentication modes**:
34
+
35
+ - **Bearer token** (default) — stored in `localStorage`, sent as `Authorization: Bearer <token>`
36
+ - **Cookie** — JWT managed by the backend as an HttpOnly cookie, **same-origin only**
37
+
38
+ Pass `mode: 'cookie'` to `login()`, `activate()`, `resetPassword()`, or `switchTeam()` only when the app is served from the same origin as the service. Since a Ulabase service lives on `*.restheart.com` while your app lives on your own domain, that cookie is third-party and is blocked by default in Safari and Firefox — regardless of the server's CORS configuration. **Cross-origin apps, which is the normal case, should stay on the default `'bearer'` mode.**
39
+
40
+ Each of these calls a matching auto-login endpoint with `delivery=body` (bearer) or `delivery=cookie`, and in bearer mode gets the fresh token back in the same response — no extra login round-trip.
41
+
42
+ For email verification, pass `delivery: 'fragment'` (default, cross-origin) or `delivery: 'cookie'` (same-origin) to `verify()`.
43
+
44
+ - `login()` stores the token in `localStorage` and schedules a proactive refresh at 80% of the token's TTL (~12 minutes for a 15-minute token).
45
+ - Every authenticated request sends `Authorization: Bearer <token>` automatically.
46
+ - Sessions survive page reloads as long as the token hasn't expired.
47
+ - If the token expires (laptop asleep, tab backgrounded too long, or the user hasn't interacted for 15+ minutes), the next API call gets a 401 and the session is cleared — the user sees "logged out," not a silent failure.
48
+
49
+ ## `RhAuthService`
50
+
51
+ Inject it anywhere and use signals directly in templates:
52
+
53
+ ```typescript
54
+ @Component({
55
+ template: `
56
+ @if (auth.isAuthenticated()) {
57
+ <span>{{ auth.user()?.profile?.firstName }}</span>
58
+ @if (auth.hasMultipleTeams()) {
59
+ <team-switcher [teams]="auth.teams()" />
60
+ }
61
+ }
62
+ `
63
+ })
64
+ export class AppComponent {
65
+ auth = inject(RhAuthService);
66
+ }
67
+ ```
68
+
69
+ ### Signals
70
+
71
+ | Signal | Type | Description |
72
+ |---|---|---|
73
+ | `user` | `Signal<UserInfo \| null>` | Authenticated user, or `null` |
74
+ | `isAuthenticated` | `Signal<boolean>` | Derived from `user` |
75
+ | `teams` | `Signal<TeamMembership[]>` | Teams the user belongs to |
76
+ | `hasMultipleTeams` | `Signal<boolean>` | `true` when user has more than one team |
77
+
78
+ ### Methods
79
+
80
+ All methods return `Observable`:
81
+
82
+ ```typescript
83
+ auth.checkSession() // Observable<UserInfo | null> — reads localStorage, no HTTP if no token
84
+ auth.login(email, password, mode?) // Observable<UserInfo> — mode: 'bearer' (default) | 'cookie'
85
+ auth.logout() // Observable<void>
86
+ auth.register(payload) // Observable<void>
87
+ auth.verify(email, token, delivery?) // Observable<string> — delivery: 'fragment' (default) | 'cookie'
88
+ auth.invite(email, role) // Observable<void>
89
+ auth.getInvitation(email, token) // Observable<Invitation>
90
+ auth.activate(payload, mode?) // Observable<void> — mode: 'bearer' (default) | 'cookie'
91
+ auth.acceptInvite(token) // Observable<void>
92
+ auth.switchTeam(teamId, mode?) // Observable<void> — mode: 'bearer' (default) | 'cookie'; re-fetches session
93
+ auth.forgotPassword(email) // Observable<void>
94
+ auth.resetPassword(payload, mode?) // Observable<void> — mode: 'bearer' (default) | 'cookie'
95
+ ```
96
+
97
+ ## Your own collections
98
+
99
+ `provideRhAuth()` registers `rhAuthInterceptor`, which applies the session to every
100
+ `HttpClient` request bound for `apiBaseUrl`: the bearer token, the challenge suppression that
101
+ keeps the browser's Basic Auth popup away on a `401`, and the cookie credentials. So your own
102
+ data requests need no header of their own:
103
+
104
+ ```typescript
105
+ private readonly http = inject(HttpClient);
106
+
107
+ load() {
108
+ return this.http.get(`${environment.apiUrl}/my-collection?pagesize=10`);
109
+ }
110
+ ```
111
+
112
+ Requests to any other host pass through untouched — the token is a credential, and attaching
113
+ it everywhere would hand it to whatever third party the app happens to call. An
114
+ `Authorization` header you set yourself is left alone. The `401` handling applies to every
115
+ request either way, because that is about the session and not the target.
116
+
117
+ Adding your own interceptor alongside it takes a second `provideHttpClient` call, *after*
118
+ `provideRhAuth`; `withInterceptors` registers each function as a `multi` provider, so the two
119
+ add up rather than replacing one another. Do not name `rhAuthInterceptor` again there — it is
120
+ already registered, and repeating it just runs it twice.
121
+
122
+ ### The kit's own calls go through `HttpClient` too
123
+
124
+ `provideRhAuth()` also hands the kit an `HttpClient`-backed transport, so a login, a session
125
+ check or a token renewal travels the same interceptor chain as everything else. Your tracing
126
+ header, retry policy or error handler covers all of it, not just the requests you wrote.
127
+
128
+ One asymmetry is deliberate: `rhAuthInterceptor` does **not** clear the session on a 401 to a
129
+ kit endpoint. A 401 there does not always mean the session is over —
130
+ `PATCH /auth/change-password` answers 401 for a wrong *current* password, `GET /token` for
131
+ wrong credentials — and signing a user out for mistyping their old password would be a poor
132
+ trade. The kit handles those itself; the interceptor's 401 handling is for your requests.
133
+
134
+ To route the kit somewhere else entirely, pass your own `transport` in the config: an explicit
135
+ one is left alone.
136
+
137
+ ```typescript
138
+ provideRhAuth({ apiBaseUrl: environment.apiUrl, transport: myTransport }),
139
+ ```
140
+
141
+ ## Guards
142
+
143
+ ```typescript
144
+ import { authGuard, publicGuard } from '@ulabase/kit-ng';
145
+
146
+ export const routes: Routes = [
147
+ {
148
+ path: 'app',
149
+ canActivate: [authGuard], // redirects to /auth/login if not authenticated
150
+ loadComponent: () => import('./shell/shell.component'),
151
+ },
152
+ {
153
+ path: 'auth/login',
154
+ canActivate: [publicGuard], // redirects to /app if already authenticated
155
+ loadComponent: () => import('./pages/login/login.component'),
156
+ },
157
+ ];
158
+ ```
159
+
160
+ `authGuard` checks the in-memory token first — no HTTP call if the user is already authenticated in the current session.
161
+
162
+ ## `RhPaymentsService`
163
+
164
+ Payments live in their own service, because a subscription is not a session. It needs the
165
+ `stripe` plugin on the service, and an explicit opt-in — without `payments: true` no
166
+ `/stripe/*` call is ever made:
167
+
168
+ ```typescript
169
+ provideRhAuth({
170
+ apiBaseUrl: 'https://my-service.restheart.com',
171
+ payments: true,
172
+ ownershipRole: 'owner', // default; set it if your deployment overrides the role
173
+ })
174
+ ```
175
+
176
+ `subscription` loads on sign-in and reloads on `switchTeam` — nothing to wire up:
177
+
178
+ ```typescript
179
+ @Component({
180
+ template: `
181
+ @if (payments.subscription(); as sub) {
182
+ <p>Plan: <strong>{{ sub.plan }}</strong> — {{ sub.active ? 'active' : 'inactive' }}</p>
183
+ @if (payments.canManageBilling()) {
184
+ <button (click)="upgrade()">Change plan</button>
185
+ }
186
+ }
187
+ `,
188
+ })
189
+ export class BillingComponent {
190
+ payments = inject(RhPaymentsService);
191
+
192
+ upgrade() {
193
+ this.payments.createCheckoutSession('gold', 'month').subscribe({
194
+ next: ({ url }) => (window.location.href = url),
195
+ // 409 means "already subscribed" — Stripe's Portal handles plan changes
196
+ error: (err) => err.status === 409 && this.openPortal(),
197
+ });
198
+ }
199
+
200
+ openPortal() {
201
+ this.payments.openBillingPortal().subscribe(({ url }) => (window.location.href = url));
202
+ }
203
+ }
204
+ ```
205
+
206
+ **Signals:** `subscription`, `plan`, `isSubscribed`, `canManageBilling`, `seatsAvailable`.
207
+
208
+ **Methods:** `loadSubscription`, `getPlans`, `createCheckoutSession`, `openBillingPortal`,
209
+ `getLicenses`, `grantLicense`, `revokeLicense`, `getCatalog`, `createOrder`, `getOrder`,
210
+ `waitForSubscription`, `waitForOrder` — each returning an `Observable`.
211
+
212
+ ### The Checkout return page
213
+
214
+ The redirect back from Stripe races the webhook, so reading `subscription()` as the page
215
+ mounts can still show the old plan. Poll instead — and treat a timeout as "not yet", not as a
216
+ failed payment:
217
+
218
+ ```typescript
219
+ this.payments.waitForSubscription(s => s.plan === 'gold' && s.active).subscribe({
220
+ next: () => this.status.set('success'),
221
+ error: (err) => this.status.set(err.name === 'WaitTimeoutError' ? 'pending' : 'error'),
222
+ });
223
+ ```
224
+
225
+ `waitForSubscription` updates the `subscription` signal itself when it resolves. See the
226
+ [core's payments guide](https://www.npmjs.com/package/@ulabase/kit#payments) for the
227
+ full reasoning.
228
+
229
+ ## Quickstart
230
+
231
+ The fastest path to a working app:
232
+
233
+ 1. Create a service on [Ulabase](https://cloud.restheart.com)
234
+ 2. Fork [`restheart-cloud-starter-ng`](https://github.com/SoftInstigate/restheart-cloud-starter-ng)
235
+ 3. Set `apiBaseUrl` in `environment.ts`
236
+ 4. `ng serve`
237
+
238
+ You get login, signup, email verification, invitations, password reset, and multi-team switching — all wired up.
@@ -0,0 +1,396 @@
1
+ import * as i0 from "@angular/core";
2
+ import { Injectable, InjectionToken, computed, effect, inject, makeEnvironmentProviders, signal, untracked } from "@angular/core";
3
+ import { catchError, firstValueFrom, from, map, of, switchMap, tap, throwError } from "rxjs";
4
+ import * as kit from "@ulabase/kit";
5
+ import { DEFAULT_CART_STORAGE_KEY, addToCart, cancelRefresh, cartTotals, clearToken, getToken, loadCart, removeFromCart, saveCart, setCartQuantity, toOrderItems } from "@ulabase/kit";
6
+ import { HttpClient, HttpContext, HttpContextToken, HttpErrorResponse, HttpHeaders, provideHttpClient, withInterceptors } from "@angular/common/http";
7
+ import { Router } from "@angular/router";
8
+ export * from "@ulabase/kit";
9
+ const RH_AUTH_CONFIG = new InjectionToken("RH_AUTH_CONFIG");
10
+ const RH_KIT_REQUEST = new HttpContextToken(() => false);
11
+ var RhAuthService = class RhAuthService {
12
+ config = inject(RH_AUTH_CONFIG);
13
+ _user = signal(null, ...ngDevMode ? [{ debugName: "_user" }] : /* istanbul ignore next */ []);
14
+ _teams = signal([], ...ngDevMode ? [{ debugName: "_teams" }] : /* istanbul ignore next */ []);
15
+ user = this._user.asReadonly();
16
+ teams = this._teams.asReadonly();
17
+ isAuthenticated = computed(() => this._user() !== null, ...ngDevMode ? [{ debugName: "isAuthenticated" }] : /* istanbul ignore next */ []);
18
+ hasMultipleTeams = computed(() => this._teams().length > 1, ...ngDevMode ? [{ debugName: "hasMultipleTeams" }] : /* istanbul ignore next */ []);
19
+ checkSession() {
20
+ if (!kit.getToken()) {
21
+ this._user.set(null);
22
+ this._teams.set([]);
23
+ return of(null);
24
+ }
25
+ return from(kit.checkSession(this.config)).pipe(tap((u) => {
26
+ this._user.set(u);
27
+ if (!u) this._teams.set([]);
28
+ }), switchMap((u) => u === null ? of([]) : from(kit.getTeams(this.config)).pipe(catchError(() => of([])))), tap((ts) => this._teams.set(ts)), map(() => this._user()));
29
+ }
30
+ register(payload) {
31
+ return from(kit.register(this.config, payload));
32
+ }
33
+ verify(email, token, delivery = "fragment") {
34
+ return from(kit.verify(this.config, email, token, delivery));
35
+ }
36
+ login(email, password, mode = "bearer") {
37
+ return from(kit.login(this.config, email, password, mode)).pipe(tap((u) => this._user.set(u)), switchMap((u) => from(kit.getTeams(this.config)).pipe(catchError(() => of([])), tap((ts) => this._teams.set(ts)), map(() => u))));
38
+ }
39
+ logout() {
40
+ return from(kit.logout(this.config)).pipe(tap(() => {
41
+ this._user.set(null);
42
+ this._teams.set([]);
43
+ }));
44
+ }
45
+ invite(email, role) {
46
+ return from(kit.invite(this.config, email, role));
47
+ }
48
+ getInvitation(email, token) {
49
+ return from(kit.getInvitation(this.config, email, token));
50
+ }
51
+ activate(payload, mode = "bearer") {
52
+ return from(kit.activate(this.config, payload, mode)).pipe(map(() => void 0));
53
+ }
54
+ acceptInvite(token) {
55
+ return from(kit.acceptInvite(this.config, token)).pipe(switchMap(() => from(kit.getTeams(this.config)).pipe(catchError(() => of([])), tap((ts) => this._teams.set(ts)))), map(() => void 0));
56
+ }
57
+ resendInvite(email) {
58
+ return from(kit.resendInvite(this.config, email));
59
+ }
60
+ listInvitations() {
61
+ return from(kit.listInvitations(this.config));
62
+ }
63
+ loadTeams() {
64
+ return from(kit.getTeams(this.config)).pipe(tap((ts) => this._teams.set(ts)));
65
+ }
66
+ switchTeam(teamId, mode = "bearer") {
67
+ return from(kit.switchTeam(this.config, teamId, mode)).pipe(switchMap(() => this.checkSession()), map(() => void 0));
68
+ }
69
+ clearSession() {
70
+ kit.clearToken();
71
+ kit.cancelRefresh();
72
+ this._user.set(null);
73
+ this._teams.set([]);
74
+ }
75
+ forgotPassword(email) {
76
+ return from(kit.forgotPassword(this.config, email));
77
+ }
78
+ resetPassword(payload, mode = "bearer") {
79
+ return from(kit.resetPassword(this.config, payload, mode)).pipe(map(() => void 0));
80
+ }
81
+ listTeamMembers() {
82
+ return from(kit.listTeamMembers(this.config));
83
+ }
84
+ removeMember(email) {
85
+ return from(kit.removeMember(this.config, email));
86
+ }
87
+ updateMemberRole(email, role) {
88
+ return from(kit.updateMemberRole(this.config, email, role));
89
+ }
90
+ createTeam(teamName) {
91
+ return from(kit.createTeam(this.config, teamName));
92
+ }
93
+ updateTeam(updates) {
94
+ return from(kit.updateTeam(this.config, updates));
95
+ }
96
+ deleteTeam() {
97
+ return from(kit.deleteTeam(this.config));
98
+ }
99
+ updateProfile(updates) {
100
+ return from(kit.updateProfile(this.config, updates)).pipe(switchMap(() => this.checkSession()), map(() => void 0));
101
+ }
102
+ changePassword(currentPassword, newPassword) {
103
+ return from(kit.changePassword(this.config, currentPassword, newPassword));
104
+ }
105
+ updateUser(email, updates) {
106
+ return from(kit.updateUser(this.config, email, updates));
107
+ }
108
+ acceptConsents(body, mode = "bearer") {
109
+ const userId = this._user()?._id ?? kit.getTokenClaims()?.["sub"];
110
+ if (!userId) return throwError(() => ({
111
+ status: 0,
112
+ message: "acceptConsents requires a signed-in user"
113
+ }));
114
+ return from(kit.acceptConsents(this.config, userId, body, mode)).pipe(tap((u) => this._user.set(u)));
115
+ }
116
+ renewToken(mode = "bearer") {
117
+ return from(kit.renewToken(this.config, mode));
118
+ }
119
+ api(path, init) {
120
+ return from(kit.apiFetch(this.config, path, init));
121
+ }
122
+ static ɵfac = i0.ɵɵngDeclareFactory({
123
+ minVersion: "12.0.0",
124
+ version: "22.2.0",
125
+ ngImport: i0,
126
+ type: RhAuthService,
127
+ deps: [],
128
+ target: i0.ɵɵFactoryTarget.Injectable
129
+ });
130
+ static ɵprov = i0.ɵɵngDeclareInjectable({
131
+ minVersion: "12.0.0",
132
+ version: "22.2.0",
133
+ ngImport: i0,
134
+ type: RhAuthService,
135
+ providedIn: "root"
136
+ });
137
+ };
138
+ i0.ɵɵngDeclareClassMetadata({
139
+ minVersion: "12.0.0",
140
+ version: "22.2.0",
141
+ ngImport: i0,
142
+ type: RhAuthService,
143
+ decorators: [{
144
+ type: Injectable,
145
+ args: [{ providedIn: "root" }]
146
+ }]
147
+ });
148
+ var RhPaymentsService = class RhPaymentsService {
149
+ config = inject(RH_AUTH_CONFIG);
150
+ auth = inject(RhAuthService);
151
+ _subscription = signal(null, ...ngDevMode ? [{ debugName: "_subscription" }] : /* istanbul ignore next */ []);
152
+ subscription = this._subscription.asReadonly();
153
+ plan = computed(() => this._subscription()?.plan ?? null, ...ngDevMode ? [{ debugName: "plan" }] : /* istanbul ignore next */ []);
154
+ isSubscribed = computed(() => this._subscription()?.active ?? false, ...ngDevMode ? [{ debugName: "isSubscribed" }] : /* istanbul ignore next */ []);
155
+ canManageBilling = computed(() => this.auth.user()?.team?.role === (this.config.ownershipRole ?? "owner"), ...ngDevMode ? [{ debugName: "canManageBilling" }] : /* istanbul ignore next */ []);
156
+ seatsAvailable = computed(() => this._subscription()?.seats?.available ?? null, ...ngDevMode ? [{ debugName: "seatsAvailable" }] : /* istanbul ignore next */ []);
157
+ teamKey = computed(() => {
158
+ const user = this.auth.user();
159
+ return user ? user.team?._id?.$oid ?? "" : null;
160
+ }, ...ngDevMode ? [{ debugName: "teamKey" }] : /* istanbul ignore next */ []);
161
+ get paymentsEnabled() {
162
+ return this.config.payments === true;
163
+ }
164
+ constructor() {
165
+ if (this.paymentsEnabled) effect(() => {
166
+ const teamKey = this.teamKey();
167
+ untracked(() => {
168
+ if (teamKey === null) this._subscription.set(null);
169
+ else this.loadSubscription().subscribe();
170
+ });
171
+ });
172
+ }
173
+ loadSubscription() {
174
+ if (!this.paymentsEnabled) return of(null);
175
+ return from(kit.getSubscription(this.config)).pipe(tap((sub) => this._subscription.set(sub)), catchError(() => {
176
+ this._subscription.set(null);
177
+ return of(null);
178
+ }));
179
+ }
180
+ getPlans() {
181
+ return from(kit.getPlans(this.config));
182
+ }
183
+ createCheckoutSession(plan, interval) {
184
+ return from(kit.createCheckoutSession(this.config, plan, interval));
185
+ }
186
+ openBillingPortal() {
187
+ return from(kit.openBillingPortal(this.config));
188
+ }
189
+ getLicenses() {
190
+ return from(kit.getLicenses(this.config));
191
+ }
192
+ grantLicense(userId) {
193
+ return from(kit.grantLicense(this.config, userId));
194
+ }
195
+ revokeLicense(userId) {
196
+ return from(kit.revokeLicense(this.config, userId));
197
+ }
198
+ getCatalog(opts) {
199
+ return from(kit.getCatalog(this.config, opts));
200
+ }
201
+ createOrder(items, email, collection) {
202
+ return from(kit.createOrder(this.config, items, email, collection));
203
+ }
204
+ getOrder(id, secret, collection) {
205
+ return from(kit.getOrder(this.config, id, secret, collection));
206
+ }
207
+ waitForSubscription(predicate, opts) {
208
+ return from(kit.waitForSubscription(this.config, predicate, opts).then((sub) => {
209
+ this._subscription.set(sub);
210
+ return sub;
211
+ }));
212
+ }
213
+ waitForOrder(id, secret, opts) {
214
+ return from(kit.waitForOrder(this.config, id, secret, opts));
215
+ }
216
+ static ɵfac = i0.ɵɵngDeclareFactory({
217
+ minVersion: "12.0.0",
218
+ version: "22.2.0",
219
+ ngImport: i0,
220
+ type: RhPaymentsService,
221
+ deps: [],
222
+ target: i0.ɵɵFactoryTarget.Injectable
223
+ });
224
+ static ɵprov = i0.ɵɵngDeclareInjectable({
225
+ minVersion: "12.0.0",
226
+ version: "22.2.0",
227
+ ngImport: i0,
228
+ type: RhPaymentsService,
229
+ providedIn: "root"
230
+ });
231
+ };
232
+ i0.ɵɵngDeclareClassMetadata({
233
+ minVersion: "12.0.0",
234
+ version: "22.2.0",
235
+ ngImport: i0,
236
+ type: RhPaymentsService,
237
+ decorators: [{
238
+ type: Injectable,
239
+ args: [{ providedIn: "root" }]
240
+ }],
241
+ ctorParameters: () => []
242
+ });
243
+ const RH_CART_STORAGE_KEY = new InjectionToken("RH_CART_STORAGE_KEY", {
244
+ providedIn: "root",
245
+ factory: () => DEFAULT_CART_STORAGE_KEY
246
+ });
247
+ var RhCartService = class RhCartService {
248
+ storageKey = inject(RH_CART_STORAGE_KEY);
249
+ _lines = signal(loadCart(this.storageKey), ...ngDevMode ? [{ debugName: "_lines" }] : /* istanbul ignore next */ []);
250
+ lines = this._lines.asReadonly();
251
+ totals = computed(() => cartTotals(this._lines()), ...ngDevMode ? [{ debugName: "totals" }] : /* istanbul ignore next */ []);
252
+ totalItems = computed(() => this.totals().totalItems, ...ngDevMode ? [{ debugName: "totalItems" }] : /* istanbul ignore next */ []);
253
+ subtotal = computed(() => this.totals().subtotal, ...ngDevMode ? [{ debugName: "subtotal" }] : /* istanbul ignore next */ []);
254
+ currency = computed(() => this.totals().currency, ...ngDevMode ? [{ debugName: "currency" }] : /* istanbul ignore next */ []);
255
+ orderItems = computed(() => toOrderItems(this._lines()), ...ngDevMode ? [{ debugName: "orderItems" }] : /* istanbul ignore next */ []);
256
+ add(item, quantity = 1) {
257
+ this.apply((current) => addToCart(current, item, quantity));
258
+ }
259
+ setQuantity(productId, quantity) {
260
+ this.apply((current) => setCartQuantity(current, productId, quantity));
261
+ }
262
+ remove(productId) {
263
+ this.apply((current) => removeFromCart(current, productId));
264
+ }
265
+ clear() {
266
+ this.apply(() => []);
267
+ }
268
+ apply(next) {
269
+ const updated = next(this._lines());
270
+ saveCart(updated, this.storageKey);
271
+ this._lines.set(updated);
272
+ }
273
+ static ɵfac = i0.ɵɵngDeclareFactory({
274
+ minVersion: "12.0.0",
275
+ version: "22.2.0",
276
+ ngImport: i0,
277
+ type: RhCartService,
278
+ deps: [],
279
+ target: i0.ɵɵFactoryTarget.Injectable
280
+ });
281
+ static ɵprov = i0.ɵɵngDeclareInjectable({
282
+ minVersion: "12.0.0",
283
+ version: "22.2.0",
284
+ ngImport: i0,
285
+ type: RhCartService,
286
+ providedIn: "root"
287
+ });
288
+ };
289
+ i0.ɵɵngDeclareClassMetadata({
290
+ minVersion: "12.0.0",
291
+ version: "22.2.0",
292
+ ngImport: i0,
293
+ type: RhCartService,
294
+ decorators: [{
295
+ type: Injectable,
296
+ args: [{ providedIn: "root" }]
297
+ }]
298
+ });
299
+ const authGuard = () => {
300
+ const auth = inject(RhAuthService);
301
+ const router = inject(Router);
302
+ if (auth.isAuthenticated()) return true;
303
+ return auth.checkSession().pipe(map((user) => user !== null), tap((ok) => {
304
+ if (!ok) router.navigate(["/auth/login"]);
305
+ }));
306
+ };
307
+ const publicGuard = () => {
308
+ const auth = inject(RhAuthService);
309
+ const router = inject(Router);
310
+ if (!auth.isAuthenticated()) return true;
311
+ return auth.checkSession().pipe(map((user) => user === null), tap((isPublic) => {
312
+ if (!isPublic) router.navigate(["/"]);
313
+ }));
314
+ };
315
+ function readToken(config) {
316
+ return config.getToken ? config.getToken() : getToken();
317
+ }
318
+ function isPromise(v) {
319
+ return typeof v?.then === "function";
320
+ }
321
+ const rhAuthInterceptor = (req, next) => {
322
+ const auth = inject(RhAuthService);
323
+ const config = inject(RH_AUTH_CONFIG);
324
+ const ownsIts401s = req.context.get(RH_KIT_REQUEST);
325
+ const onError = (source) => source.pipe(catchError((err) => {
326
+ if (!ownsIts401s && err instanceof HttpErrorResponse && err.status === 401) {
327
+ clearToken();
328
+ cancelRefresh();
329
+ auth.clearSession();
330
+ }
331
+ return throwError(() => err);
332
+ }));
333
+ if (!req.url.startsWith(config.apiBaseUrl)) return next(req).pipe(onError);
334
+ const withCredentials = (token) => next(req.clone({
335
+ setHeaders: {
336
+ ...token && !req.headers.has("Authorization") ? { Authorization: `Bearer ${token}` } : {},
337
+ ...req.headers.has("No-Auth-Challenge") ? {} : { "No-Auth-Challenge": "true" }
338
+ },
339
+ withCredentials: true
340
+ })).pipe(onError);
341
+ const token = readToken(config);
342
+ return isPromise(token) ? from(token).pipe(switchMap(withCredentials)) : withCredentials(token);
343
+ };
344
+ const NULL_BODY_STATUS = /* @__PURE__ */ new Set([
345
+ 101,
346
+ 103,
347
+ 204,
348
+ 205,
349
+ 304
350
+ ]);
351
+ function toHttpHeaders(source) {
352
+ let headers = new HttpHeaders();
353
+ new Headers(source).forEach((value, name) => {
354
+ headers = headers.set(name, value);
355
+ });
356
+ return headers;
357
+ }
358
+ function toHeaders(source) {
359
+ const headers = new Headers();
360
+ for (const name of source.keys()) for (const value of source.getAll(name) ?? []) headers.append(name, value);
361
+ return headers;
362
+ }
363
+ function toResponse(body, status, statusText, headers) {
364
+ return new Response(NULL_BODY_STATUS.has(status) ? null : body, {
365
+ status,
366
+ statusText,
367
+ headers: toHeaders(headers)
368
+ });
369
+ }
370
+ function httpClientTransport(http) {
371
+ return (url, init) => {
372
+ return firstValueFrom(http.request(init?.method ?? "GET", url, {
373
+ body: init?.body ?? null,
374
+ headers: toHttpHeaders(init?.headers),
375
+ observe: "response",
376
+ responseType: "text",
377
+ context: new HttpContext().set(RH_KIT_REQUEST, true),
378
+ withCredentials: true
379
+ })).then((res) => toResponse(res.body, res.status, res.statusText, res.headers), (err) => {
380
+ if (err instanceof HttpErrorResponse && err.status !== 0) return toResponse(typeof err.error === "string" ? err.error : JSON.stringify(err.error ?? null), err.status, err.statusText, err.headers);
381
+ throw err;
382
+ });
383
+ };
384
+ }
385
+ function provideRhAuth(config) {
386
+ return makeEnvironmentProviders([provideHttpClient(withInterceptors([rhAuthInterceptor])), {
387
+ provide: RH_AUTH_CONFIG,
388
+ useFactory: () => ({
389
+ ...config,
390
+ transport: config.transport ?? httpClientTransport(inject(HttpClient))
391
+ })
392
+ }]);
393
+ }
394
+ export { RH_AUTH_CONFIG, RH_CART_STORAGE_KEY, RhAuthService, RhCartService, RhPaymentsService, authGuard, httpClientTransport, provideRhAuth, publicGuard, rhAuthInterceptor };
395
+
396
+ //# sourceMappingURL=ulabase-kit-ng.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ulabase-kit-ng.mjs","names":[],"sources":["../../src/tokens.ts","../../src/auth.service.ts","../../src/payments.service.ts","../../src/cart.service.ts","../../src/auth.guard.ts","../../src/auth.interceptor.ts","../../src/http-transport.ts","../../src/provide-rh-auth.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport { HttpContextToken } from '@angular/common/http';\nimport type { AuthConfig } from '@ulabase/kit';\n\nexport const RH_AUTH_CONFIG = new InjectionToken<AuthConfig>('RH_AUTH_CONFIG');\n\n\n/**\n * Marks a request as originating from the kit rather than from application\n * code. Set by `httpClientTransport`, read by `rhAuthInterceptor`.\n *\n * The kit owns the meaning of a 401 on its own endpoints, and that meaning is\n * not always \"the session is dead\": `PATCH /auth/change-password` answers 401\n * when the *current password* is wrong, and `GET /token` when the credentials\n * are. Letting the interceptor clear the session on those would sign a user\n * out for mistyping their old password.\n */\nexport const RH_KIT_REQUEST = new HttpContextToken<boolean>(() => false);\n","import { Injectable, computed, inject, signal } from '@angular/core';\nimport { Observable, catchError, from, map, of, switchMap, tap, throwError } from 'rxjs';\nimport type { UserInfo, TeamMembership, TeamMember, PendingInvitation, AuthConfig, LoginMode } from '@ulabase/kit';\nimport * as kit from '@ulabase/kit';\nimport { RH_AUTH_CONFIG } from './tokens.js';\n\n@Injectable({ providedIn: 'root' })\nexport class RhAuthService {\n private readonly config: AuthConfig = inject(RH_AUTH_CONFIG);\n\n private readonly _user = signal<UserInfo | null>(null);\n private readonly _teams = signal<TeamMembership[]>([]);\n\n readonly user = this._user.asReadonly();\n readonly teams = this._teams.asReadonly();\n readonly isAuthenticated = computed(() => this._user() !== null);\n readonly hasMultipleTeams = computed(() => this._teams().length > 1);\n\n /**\n * Check the current session state.\n *\n * Reads the token from localStorage — if present and not expired,\n * returns user info from the server. Otherwise returns null.\n */\n checkSession(): Observable<UserInfo | null> {\n // If no valid token in localStorage, we're logged out — no HTTP call needed\n if (!kit.getToken()) {\n this._user.set(null);\n this._teams.set([]);\n return of(null);\n }\n\n return from(kit.checkSession(this.config)).pipe(\n tap(u => {\n this._user.set(u);\n if (!u) {\n this._teams.set([]);\n }\n }),\n switchMap(u =>\n u === null\n ? of([])\n : from(kit.getTeams(this.config)).pipe(catchError(() => of([])))\n ),\n tap(ts => this._teams.set(ts)),\n map(() => this._user())\n );\n }\n\n register(payload: {\n email: string;\n password: string;\n teamName: string;\n firstName: string;\n lastName: string;\n [key: string]: unknown;\n }): Observable<void> {\n return from(kit.register(this.config, payload));\n }\n\n verify(email: string, token: string, delivery: 'cookie' | 'fragment' = 'fragment'): Observable<string> {\n return from(kit.verify(this.config, email, token, delivery));\n }\n\n login(email: string, password: string, mode: LoginMode = 'bearer'): Observable<UserInfo> {\n return from(kit.login(this.config, email, password, mode)).pipe(\n tap(u => this._user.set(u)),\n switchMap(u =>\n from(kit.getTeams(this.config)).pipe(\n catchError(() => of([])),\n tap(ts => this._teams.set(ts)),\n map(() => u)\n )\n )\n );\n }\n\n logout(): Observable<void> {\n return from(kit.logout(this.config)).pipe(\n tap(() => {\n this._user.set(null);\n this._teams.set([]);\n })\n );\n }\n\n invite(email: string, role: 'owner' | 'member'): Observable<void> {\n return from(kit.invite(this.config, email, role));\n }\n\n getInvitation(email: string, token: string) {\n return from(kit.getInvitation(this.config, email, token));\n }\n\n activate(payload: { email: string; token: string; password: string }, mode: LoginMode = 'bearer'): Observable<void> {\n return from(kit.activate(this.config, payload, mode)).pipe(map(() => undefined));\n }\n\n acceptInvite(token: string): Observable<void> {\n return from(kit.acceptInvite(this.config, token)).pipe(\n switchMap(() =>\n from(kit.getTeams(this.config)).pipe(\n catchError(() => of([])),\n tap(ts => this._teams.set(ts))\n )\n ),\n map(() => undefined)\n );\n }\n\n resendInvite(email: string): Observable<void> {\n return from(kit.resendInvite(this.config, email));\n }\n\n listInvitations(): Observable<PendingInvitation[]> {\n return from(kit.listInvitations(this.config));\n }\n\n loadTeams(): Observable<TeamMembership[]> {\n return from(kit.getTeams(this.config)).pipe(\n tap(ts => this._teams.set(ts))\n );\n }\n\n switchTeam(teamId: { $oid: string }, mode: LoginMode = 'bearer'): Observable<void> {\n return from(kit.switchTeam(this.config, teamId, mode)).pipe(\n switchMap(() => this.checkSession()),\n map(() => undefined)\n );\n }\n\n clearSession(): void {\n kit.clearToken();\n kit.cancelRefresh();\n this._user.set(null);\n this._teams.set([]);\n }\n\n forgotPassword(email: string): Observable<void> {\n return from(kit.forgotPassword(this.config, email));\n }\n\n resetPassword(payload: { email: string; token: string; password: string }, mode: LoginMode = 'bearer'): Observable<void> {\n return from(kit.resetPassword(this.config, payload, mode)).pipe(map(() => undefined));\n }\n\n // ── Team members ─────────────────────────────────────────────────────────\n\n listTeamMembers(): Observable<TeamMember[]> {\n return from(kit.listTeamMembers(this.config));\n }\n\n removeMember(email: string): Observable<void> {\n return from(kit.removeMember(this.config, email));\n }\n\n updateMemberRole(email: string, role: 'owner' | 'member'): Observable<void> {\n return from(kit.updateMemberRole(this.config, email, role));\n }\n\n // ── Team lifecycle ───────────────────────────────────────────────────────\n\n createTeam(teamName: string): Observable<TeamMembership> {\n return from(kit.createTeam(this.config, teamName));\n }\n\n updateTeam(updates: { name?: string; description?: string }): Observable<void> {\n return from(kit.updateTeam(this.config, updates));\n }\n\n deleteTeam(): Observable<void> {\n return from(kit.deleteTeam(this.config));\n }\n\n // ── Profile & password ──────────────────────────────────────────────────\n\n updateProfile(updates: { firstName?: string; lastName?: string }): Observable<void> {\n return from(kit.updateProfile(this.config, updates)).pipe(\n switchMap(() => this.checkSession()),\n map(() => undefined)\n );\n }\n\n changePassword(currentPassword: string, newPassword: string): Observable<void> {\n return from(kit.changePassword(this.config, currentPassword, newPassword));\n }\n\n updateUser(email: string, updates: Record<string, unknown>): Observable<void> {\n return from(kit.updateUser(this.config, email, updates));\n }\n\n // ── Consents ────────────────────────────────────────────────────────────\n\n /**\n * Record the signed-in user's acceptance of the application's consents,\n * renew the token so the guard sees it, and update the `user` signal.\n */\n acceptConsents(body?: Record<string, unknown>, mode: LoginMode = 'bearer'): Observable<UserInfo> {\n // The user document may be missing precisely because the rule is blocking\n // `/users/me` — the case this call exists to get out of. The token still\n // says who they are.\n const userId = this._user()?._id ?? (kit.getTokenClaims()?.['sub'] as string | undefined);\n if (!userId) {\n return throwError(() => ({ status: 0, message: 'acceptConsents requires a signed-in user' }));\n }\n return from(kit.acceptConsents(this.config, userId, body, mode)).pipe(\n tap(u => this._user.set(u))\n );\n }\n\n /** Force a new token, carrying the user document as it is now. */\n renewToken(mode: LoginMode = 'bearer'): Observable<string | null> {\n return from(kit.renewToken(this.config, mode));\n }\n\n /**\n * An authenticated `fetch` against the service, returned as an Observable.\n *\n * The Angular counterpart of React's `auth.api()` — the bearer token is\n * attached automatically, and the request goes through the Angular\n * interceptor chain (when `httpClientTransport` is configured).\n *\n * ```ts\n * this.auth.api('/demo').pipe(\n * switchMap(res => res.json()),\n * ).subscribe(data => console.log(data));\n * ```\n *\n * For a POST:\n * ```ts\n * this.auth.api('/demo', {\n * method: 'POST',\n * body: JSON.stringify({ name: 'hello' }),\n * }).pipe(switchMap(res => res.json()));\n * ```\n *\n * Rejects with an `ApiError` (`{ status, message }`) on any non-2xx response.\n */\n api(path: string, init?: RequestInit): Observable<Response> {\n return from(kit.apiFetch(this.config, path, init));\n }\n}\n","import { Injectable, computed, effect, inject, signal, untracked } from '@angular/core';\nimport { Observable, catchError, from, of, tap } from 'rxjs';\nimport type {\n AuthConfig,\n CatalogItem,\n GrantLicenseResult,\n Licenses,\n Order,\n Plan,\n Subscription,\n WaitOptions,\n CatalogQuery,\n} from '@ulabase/kit';\nimport * as kit from '@ulabase/kit';\nimport { RH_AUTH_CONFIG } from './tokens.js';\nimport { RhAuthService } from './auth.service.js';\n\n/**\n * Subscription, billing and order management.\n *\n * Separated from {@link RhAuthService} because payments are not authentication.\n * Reads the current user from {@link RhAuthService} to derive\n * `canManageBilling`, but owns its own subscription state.\n *\n * Subscription is loaded automatically when the user signs in and reloaded\n * when they switch team (via an `effect` watching {@link teamKey}). No manual\n * wiring needed.\n *\n * Only active when `config.payments` is `true` — otherwise every method\n * is a no-op and no `/stripe/*` call is ever made.\n */\n@Injectable({ providedIn: 'root' })\nexport class RhPaymentsService {\n private readonly config: AuthConfig = inject(RH_AUTH_CONFIG);\n private readonly auth = inject(RhAuthService);\n\n private readonly _subscription = signal<Subscription | null>(null);\n\n readonly subscription = this._subscription.asReadonly();\n readonly plan = computed(() => this._subscription()?.plan ?? null);\n readonly isSubscribed = computed(() => this._subscription()?.active ?? false);\n readonly canManageBilling = computed(\n () => this.auth.user()?.team?.role === (this.config.ownershipRole ?? 'owner')\n );\n readonly seatsAvailable = computed(() => this._subscription()?.seats?.available ?? null);\n\n /**\n * The team the subscription belongs to: `null` when signed out, `''` for a\n * signed-in user with no team.\n *\n * The effect below tracks this rather than `auth.user()` itself. Both\n * `updateProfile` and `acceptConsents` re-run `checkSession`, which replaces\n * the user with a freshly parsed document — tracking that object's identity\n * would reload the subscription on every profile edit and consent\n * acceptance, which the adapter contract rules out. A subscription changes\n * with the team, not with the profile.\n */\n private readonly teamKey = computed(() => {\n const user = this.auth.user();\n return user ? (user.team?._id?.$oid ?? '') : null;\n });\n\n private get paymentsEnabled(): boolean {\n return this.config.payments === true;\n }\n\n constructor() {\n if (this.paymentsEnabled) {\n effect(() => {\n const teamKey = this.teamKey();\n untracked(() => {\n if (teamKey === null) {\n this._subscription.set(null);\n } else {\n this.loadSubscription().subscribe();\n }\n });\n });\n }\n }\n\n /** Reload the team's subscription. No-op when `config.payments` is not `true`. */\n loadSubscription(): Observable<Subscription | null> {\n if (!this.paymentsEnabled) return of(null);\n return from(kit.getSubscription(this.config)).pipe(\n tap(sub => this._subscription.set(sub)),\n catchError(() => {\n this._subscription.set(null);\n return of(null);\n })\n );\n }\n\n /** The service's subscription plan catalog. No session required. */\n getPlans(): Observable<{ default_plan: string; plans: Plan[] }> {\n return from(kit.getPlans(this.config));\n }\n\n /**\n * Start a Stripe Checkout session.\n *\n * **Rejects with `status: 409`** when the team already has an active\n * subscription — send them to {@link openBillingPortal} instead.\n */\n createCheckoutSession(plan: string, interval: 'month' | 'year'): Observable<{ url: string }> {\n return from(kit.createCheckoutSession(this.config, plan, interval));\n }\n\n /** Open the Stripe Customer Portal for self-service plan changes. */\n openBillingPortal(): Observable<{ url: string }> {\n return from(kit.openBillingPortal(this.config));\n }\n\n /** The team's seat licences. Requires `canManageBilling`. */\n getLicenses(): Observable<Licenses> {\n return from(kit.getLicenses(this.config));\n }\n\n /**\n * Grant a seat licence. Returns `'granted'` or `'already-licensed'`.\n * Rejects with `status: 409` when no seat is available.\n */\n grantLicense(userId: string): Observable<GrantLicenseResult> {\n return from(kit.grantLicense(this.config, userId));\n }\n\n /** Revoke a seat licence. */\n revokeLicense(userId: string): Observable<void> {\n return from(kit.revokeLicense(this.config, userId));\n }\n\n /** Read the product catalog. */\n getCatalog(opts?: CatalogQuery): Observable<CatalogItem[]> {\n return from(kit.getCatalog(this.config, opts));\n }\n\n /**\n * Create an order and start Checkout.\n *\n * @param email Required for guest checkout.\n */\n createOrder(\n items: { productId: string; quantity: number }[],\n email?: string,\n collection?: string\n ): Observable<{ _id: { $oid: string }; checkout_url: string; secret: string }> {\n return from(kit.createOrder(this.config, items, email, collection));\n }\n\n /** Read an order back. */\n getOrder(id: string, secret?: string, collection?: string): Observable<Order> {\n return from(kit.getOrder(this.config, id, secret, collection));\n }\n\n /**\n * Poll until the subscription satisfies `predicate`.\n *\n * Use on the Checkout success page — the redirect back from Stripe races\n * the webhook, so a bare `getSubscription` can still show the old plan.\n *\n * **Timing out is not a payment failure.** Rejects with\n * `WaitTimeoutError` when the webhook hasn't arrived yet.\n */\n waitForSubscription(\n predicate: (subscription: Subscription) => boolean,\n opts?: WaitOptions\n ): Observable<Subscription> {\n return from(\n kit.waitForSubscription(this.config, predicate, opts).then(sub => {\n this._subscription.set(sub);\n return sub;\n })\n );\n }\n\n /**\n * Poll until the order leaves `'pending_payment'`.\n *\n * Same race as {@link waitForSubscription} — use on the order success page.\n *\n * **Timing out is not a payment failure.** Rejects with\n * `WaitTimeoutError` when the webhook hasn't arrived yet.\n */\n waitForOrder(\n id: string,\n secret?: string,\n opts?: WaitOptions & { collection?: string }\n ): Observable<Order> {\n return from(kit.waitForOrder(this.config, id, secret, opts));\n }\n}\n","import { Injectable, InjectionToken, computed, inject, signal } from '@angular/core';\nimport {\n addToCart,\n cartTotals,\n loadCart,\n removeFromCart,\n saveCart,\n setCartQuantity,\n toOrderItems,\n DEFAULT_CART_STORAGE_KEY,\n type CartItem,\n type CartLine,\n} from '@ulabase/kit';\n/**\n * Where {@link RhCartService} keeps the cart in `localStorage`. Defaults to `'rh-cart'`.\n *\n * Worth providing when two of your apps share an origin, which is one deployment decision away\n * from happening by accident.\n *\n * Declared here rather than in `tokens.ts`: that file is the authentication surface, and a cart\n * needs no session. Importing the kit into it to reach one constant also dragged the kit's module\n * into every spec that touches `RH_AUTH_CONFIG`, where it is auto-mocked — which shifted module\n * initialisation enough to make two unrelated guard tests fail, in CI only.\n */\nexport const RH_CART_STORAGE_KEY = new InjectionToken<string>('RH_CART_STORAGE_KEY', {\n providedIn: 'root',\n factory: () => DEFAULT_CART_STORAGE_KEY,\n});\n\n/**\n * The shopping cart.\n *\n * Independent of {@link import('./auth.service.js').RhAuthService | RhAuthService}\n * and of `RH_AUTH_CONFIG`: a cart belongs to the browser, not to a session, and\n * a shop that makes people sign in before they can put something in a basket\n * loses most of them there. It becomes an order — which does need a config —\n * when {@link RhCartService.orderItems} is handed to `createOrder`.\n *\n * On the server, `localStorage` does not exist and the cart reads as empty.\n * That is correct for a rendered page — a request carries no basket — but it\n * does mean a server-rendered cart count starts at zero and fills in once the\n * browser takes over.\n */\n@Injectable({ providedIn: 'root' })\nexport class RhCartService {\n private readonly storageKey = inject(RH_CART_STORAGE_KEY);\n\n // Declared after `storageKey` so the field is there: initializers run in order.\n private readonly _lines = signal<CartLine[]>(loadCart(this.storageKey));\n\n /** The lines, in the order they were added. */\n readonly lines = this._lines.asReadonly();\n\n private readonly totals = computed(() => cartTotals(this._lines()));\n\n /** Units, not lines: two of one thing counts two. */\n readonly totalItems = computed(() => this.totals().totalItems);\n /** Display only. Minor units, meaningful when every line shares a currency. */\n readonly subtotal = computed(() => this.totals().subtotal);\n /** The first line's currency, or `'eur'` when empty. */\n readonly currency = computed(() => this.totals().currency);\n /** The cart as `createOrder` wants it. */\n readonly orderItems = computed(() => toOrderItems(this._lines()));\n\n /** Adds an item, or increases the line already holding it. */\n add(item: CartItem, quantity = 1): void {\n this.apply(current => addToCart(current, item, quantity));\n }\n\n /** Sets a line's quantity. Zero removes it. */\n setQuantity(productId: string, quantity: number): void {\n this.apply(current => setCartQuantity(current, productId, quantity));\n }\n\n remove(productId: string): void {\n this.apply(current => removeFromCart(current, productId));\n }\n\n clear(): void {\n this.apply(() => []);\n }\n\n /**\n * State and storage move together, in the operation that changed them.\n *\n * Deliberately not an `effect` reacting to the signal: an effect runs after\n * the fact and can be skipped or reordered, and the failure that produces is\n * a line coming back from storage after somebody removed it.\n */\n private apply(next: (current: CartLine[]) => CartLine[]): void {\n const updated = next(this._lines());\n saveCart(updated, this.storageKey);\n this._lines.set(updated);\n }\n}\n","import { inject } from '@angular/core';\nimport { Router, type CanActivateFn } from '@angular/router';\nimport { map, tap } from 'rxjs';\nimport { RhAuthService } from './auth.service.js';\n\nexport const authGuard: CanActivateFn = () => {\n const auth = inject(RhAuthService);\n const router = inject(Router);\n\n if (auth.isAuthenticated()) return true;\n\n return auth.checkSession().pipe(\n map(user => user !== null),\n tap(ok => { if (!ok) router.navigate(['/auth/login']); })\n );\n};\n\nexport const publicGuard: CanActivateFn = () => {\n const auth = inject(RhAuthService);\n const router = inject(Router);\n\n if (!auth.isAuthenticated()) return true;\n\n return auth.checkSession().pipe(\n map(user => user === null),\n tap(isPublic => { if (!isPublic) router.navigate(['/']); })\n );\n};\n","import { inject } from '@angular/core';\nimport {\n HttpRequest,\n HttpHandlerFn,\n HttpInterceptorFn,\n HttpErrorResponse,\n} from '@angular/common/http';\nimport { Observable, catchError, from, switchMap, throwError } from 'rxjs';\nimport { RhAuthService } from './auth.service.js';\nimport { RH_AUTH_CONFIG, RH_KIT_REQUEST } from './tokens.js';\nimport { clearToken, cancelRefresh, getToken } from '@ulabase/kit';\nimport type { AuthConfig } from '@ulabase/kit';\n\n/** The token source is pluggable and may be async — keep the sync path sync. */\nfunction readToken(config: AuthConfig): string | null | Promise<string | null> {\n return config.getToken ? config.getToken() : getToken();\n}\n\nfunction isPromise<T>(v: T | Promise<T>): v is Promise<T> {\n return typeof (v as Promise<T>)?.then === 'function';\n}\n\n/**\n * Authenticates the application's own `HttpClient` requests, and clears the\n * session when the service rejects one with a 401.\n *\n * The kit's own calls go out through `fetch` and never reach an Angular\n * interceptor, so without this an app querying its own collections had to\n * attach the bearer token by hand at every call site.\n *\n * **Only requests to `apiBaseUrl` are touched.** The token is a credential:\n * attaching it to every outgoing request would hand it to any third-party host\n * the app happens to call. Requests elsewhere pass through untouched — they\n * still get the 401 handling, which is about the session and not the target.\n *\n * An `Authorization` header already on the request is left alone, so a call\n * that needs different credentials can say so.\n */\nexport const rhAuthInterceptor: HttpInterceptorFn = (\n req: HttpRequest<unknown>,\n next: HttpHandlerFn\n) => {\n const auth = inject(RhAuthService);\n const config = inject(RH_AUTH_CONFIG);\n\n // A 401 on the kit's own endpoints does not always mean the session is dead\n // — change-password and token answer 401 for wrong *supplied* credentials —\n // and the kit already handles those. Clearing here would sign a user out for\n // mistyping their old password.\n const ownsIts401s = req.context.get(RH_KIT_REQUEST);\n\n const onError = (source: Observable<unknown>) =>\n source.pipe(\n catchError((err: unknown) => {\n if (!ownsIts401s && err instanceof HttpErrorResponse && err.status === 401) {\n clearToken();\n cancelRefresh();\n auth.clearSession();\n }\n return throwError(() => err);\n })\n );\n\n if (!req.url.startsWith(config.apiBaseUrl)) {\n return next(req).pipe(onError) as ReturnType<HttpHandlerFn>;\n }\n\n const withCredentials = (token: string | null) =>\n next(\n req.clone({\n setHeaders: {\n ...(token && !req.headers.has('Authorization')\n ? { Authorization: `Bearer ${token}` }\n : {}),\n // Suppress RESTHeart's WWW-Authenticate challenge on a 401 —\n // without it the browser shows its native Basic Auth popup.\n ...(req.headers.has('No-Auth-Challenge') ? {} : { 'No-Auth-Challenge': 'true' }),\n },\n // Matches the kit's own `credentials: 'include'`, so a cookie-mode\n // session authenticates these requests too.\n withCredentials: true,\n })\n ).pipe(onError) as ReturnType<HttpHandlerFn>;\n\n const token = readToken(config);\n return isPromise(token)\n ? (from(token).pipe(switchMap(withCredentials)) as ReturnType<HttpHandlerFn>)\n : withCredentials(token);\n};\n","import {\n HttpClient,\n HttpContext,\n HttpErrorResponse,\n HttpHeaders,\n HttpResponse,\n} from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport type { AuthConfig } from '@ulabase/kit';\nimport { RH_KIT_REQUEST } from './tokens.js';\n\n/** Statuses the Fetch spec forbids a body on — `new Response(body, …)` throws. */\nconst NULL_BODY_STATUS = new Set([101, 103, 204, 205, 304]);\n\nfunction toHttpHeaders(source: HeadersInit | undefined): HttpHeaders {\n let headers = new HttpHeaders();\n new Headers(source).forEach((value, name) => {\n headers = headers.set(name, value);\n });\n return headers;\n}\n\nfunction toHeaders(source: HttpHeaders): Headers {\n const headers = new Headers();\n for (const name of source.keys()) {\n for (const value of source.getAll(name) ?? []) {\n headers.append(name, value);\n }\n }\n return headers;\n}\n\nfunction toResponse(body: string | null, status: number, statusText: string, headers: HttpHeaders): Response {\n return new Response(NULL_BODY_STATUS.has(status) ? null : body, {\n status,\n statusText,\n headers: toHeaders(headers),\n });\n}\n\n/**\n * A {@link AuthConfig.transport} backed by Angular's `HttpClient`, so the kit's\n * own calls go through the interceptor chain like everything else the\n * application sends.\n *\n * Without it the kit speaks `fetch` directly and an interceptor never sees a\n * login, a session check or a token renewal — which makes any cross-cutting\n * concern written as an interceptor quietly partial.\n *\n * Two mismatches with `fetch` are reconciled here:\n *\n * - **`HttpClient` throws on a non-2xx response.** `fetch` resolves and lets\n * the caller read the status, which is what the core does to build its\n * `ApiError`. So an `HttpErrorResponse` carrying a real response is turned\n * back into a resolved `Response`.\n * - **`status === 0` is not a response**, it is a network or CORS failure with\n * nothing behind it. `new Response` would reject the status anyway, so the\n * original error is rethrown and surfaces as a rejected promise, exactly as\n * a failed `fetch` would.\n *\n * The body is read as text and left unparsed: the core calls `.json()` itself,\n * and on an error response it needs the raw body to pull out the message.\n */\nexport function httpClientTransport(http: HttpClient) {\n return (url: string, init?: RequestInit): Promise<Response> => {\n const request = firstValueFrom(\n http.request(init?.method ?? 'GET', url, {\n body: init?.body ?? null,\n headers: toHttpHeaders(init?.headers as HeadersInit | undefined),\n observe: 'response',\n responseType: 'text',\n // The kit handles 401s on its own endpoints; see RH_KIT_REQUEST.\n context: new HttpContext().set(RH_KIT_REQUEST, true),\n // Mirrors the core's `credentials: 'include'` — cookie-mode sessions\n // authenticate on the cookie alone.\n withCredentials: true,\n })\n );\n\n return request.then(\n (res: HttpResponse<string>) => toResponse(res.body, res.status, res.statusText, res.headers),\n (err: unknown) => {\n if (err instanceof HttpErrorResponse && err.status !== 0) {\n const body = typeof err.error === 'string' ? err.error : JSON.stringify(err.error ?? null);\n return toResponse(body, err.status, err.statusText, err.headers);\n }\n throw err;\n }\n );\n };\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';\nimport type { AuthConfig } from '@ulabase/kit';\nimport { RH_AUTH_CONFIG } from './tokens.js';\nimport { rhAuthInterceptor } from './auth.interceptor.js';\nimport { httpClientTransport } from './http-transport.js';\n\nexport function provideRhAuth(config: AuthConfig): EnvironmentProviders {\n return makeEnvironmentProviders([\n provideHttpClient(withInterceptors([rhAuthInterceptor])),\n {\n provide: RH_AUTH_CONFIG,\n // A factory, not a value, so the kit's own calls can be routed through\n // `HttpClient` — which does not exist yet when the caller builds the\n // config. An explicit `transport` is left alone, so an application can\n // still supply its own.\n useFactory: (): AuthConfig => ({\n ...config,\n transport: config.transport ?? httpClientTransport(inject(HttpClient)),\n }),\n },\n ]);\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@ulabase/kit-ng",
3
+ "version": "0.12.0",
4
+ "description": "Angular services, route guards, and HTTP interceptor for Ulabase authentication and payments — wraps @ulabase/kit.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/ulabase/kit.git",
8
+ "directory": "packages/kit-ng"
9
+ },
10
+ "license": "MIT",
11
+ "peerDependencies": {
12
+ "@angular/common": ">=21",
13
+ "@angular/core": ">=21",
14
+ "@angular/router": ">=21",
15
+ "rxjs": ">=7"
16
+ },
17
+ "dependencies": {
18
+ "@ulabase/kit": "0.12.0",
19
+ "tslib": "^2.3.0"
20
+ },
21
+ "module": "fesm2022/ulabase-kit-ng.mjs",
22
+ "typings": "types/ulabase-kit-ng.d.ts",
23
+ "exports": {
24
+ "./package.json": {
25
+ "default": "./package.json"
26
+ },
27
+ ".": {
28
+ "types": "./types/ulabase-kit-ng.d.ts",
29
+ "default": "./fesm2022/ulabase-kit-ng.mjs"
30
+ }
31
+ },
32
+ "sideEffects": false,
33
+ "type": "module"
34
+ }
@@ -0,0 +1,326 @@
1
+ import { Observable } from "rxjs";
2
+ import * as kit from "@ulabase/kit";
3
+ import { AuthConfig, CartItem, CartLine, CatalogItem, CatalogQuery, GrantLicenseResult, Licenses, LoginMode, Order, PendingInvitation, Plan, Subscription, TeamMember, TeamMembership, UserInfo, WaitOptions } from "@ulabase/kit";
4
+ import * as i0 from "@angular/core";
5
+ import { EnvironmentProviders, InjectionToken } from "@angular/core";
6
+ import { CanActivateFn } from "@angular/router";
7
+ import { HttpClient, HttpContextToken, HttpInterceptorFn } from "@angular/common/http";
8
+ export * from "@ulabase/kit";
9
+ export declare class RhAuthService {
10
+ private readonly config;
11
+ private readonly _user;
12
+ private readonly _teams;
13
+ readonly user: import("@angular/core").Signal<UserInfo | null>;
14
+ readonly teams: import("@angular/core").Signal<TeamMembership[]>;
15
+ readonly isAuthenticated: import("@angular/core").Signal<boolean>;
16
+ readonly hasMultipleTeams: import("@angular/core").Signal<boolean>;
17
+ /**
18
+ * Check the current session state.
19
+ *
20
+ * Reads the token from localStorage — if present and not expired,
21
+ * returns user info from the server. Otherwise returns null.
22
+ */
23
+ checkSession(): Observable<UserInfo | null>;
24
+ register(payload: {
25
+ email: string;
26
+ password: string;
27
+ teamName: string;
28
+ firstName: string;
29
+ lastName: string;
30
+ [key: string]: unknown;
31
+ }): Observable<void>;
32
+ verify(email: string, token: string, delivery?: 'cookie' | 'fragment'): Observable<string>;
33
+ login(email: string, password: string, mode?: LoginMode): Observable<UserInfo>;
34
+ logout(): Observable<void>;
35
+ invite(email: string, role: 'owner' | 'member'): Observable<void>;
36
+ getInvitation(email: string, token: string): Observable<kit.Invitation>;
37
+ activate(payload: {
38
+ email: string;
39
+ token: string;
40
+ password: string;
41
+ }, mode?: LoginMode): Observable<void>;
42
+ acceptInvite(token: string): Observable<void>;
43
+ resendInvite(email: string): Observable<void>;
44
+ listInvitations(): Observable<PendingInvitation[]>;
45
+ loadTeams(): Observable<TeamMembership[]>;
46
+ switchTeam(teamId: {
47
+ $oid: string;
48
+ }, mode?: LoginMode): Observable<void>;
49
+ clearSession(): void;
50
+ forgotPassword(email: string): Observable<void>;
51
+ resetPassword(payload: {
52
+ email: string;
53
+ token: string;
54
+ password: string;
55
+ }, mode?: LoginMode): Observable<void>;
56
+ listTeamMembers(): Observable<TeamMember[]>;
57
+ removeMember(email: string): Observable<void>;
58
+ updateMemberRole(email: string, role: 'owner' | 'member'): Observable<void>;
59
+ createTeam(teamName: string): Observable<TeamMembership>;
60
+ updateTeam(updates: {
61
+ name?: string;
62
+ description?: string;
63
+ }): Observable<void>;
64
+ deleteTeam(): Observable<void>;
65
+ updateProfile(updates: {
66
+ firstName?: string;
67
+ lastName?: string;
68
+ }): Observable<void>;
69
+ changePassword(currentPassword: string, newPassword: string): Observable<void>;
70
+ updateUser(email: string, updates: Record<string, unknown>): Observable<void>;
71
+ /**
72
+ * Record the signed-in user's acceptance of the application's consents,
73
+ * renew the token so the guard sees it, and update the `user` signal.
74
+ */
75
+ acceptConsents(body?: Record<string, unknown>, mode?: LoginMode): Observable<UserInfo>;
76
+ /** Force a new token, carrying the user document as it is now. */
77
+ renewToken(mode?: LoginMode): Observable<string | null>;
78
+ /**
79
+ * An authenticated `fetch` against the service, returned as an Observable.
80
+ *
81
+ * The Angular counterpart of React's `auth.api()` — the bearer token is
82
+ * attached automatically, and the request goes through the Angular
83
+ * interceptor chain (when `httpClientTransport` is configured).
84
+ *
85
+ * ```ts
86
+ * this.auth.api('/demo').pipe(
87
+ * switchMap(res => res.json()),
88
+ * ).subscribe(data => console.log(data));
89
+ * ```
90
+ *
91
+ * For a POST:
92
+ * ```ts
93
+ * this.auth.api('/demo', {
94
+ * method: 'POST',
95
+ * body: JSON.stringify({ name: 'hello' }),
96
+ * }).pipe(switchMap(res => res.json()));
97
+ * ```
98
+ *
99
+ * Rejects with an `ApiError` (`{ status, message }`) on any non-2xx response.
100
+ */
101
+ api(path: string, init?: RequestInit): Observable<Response>;
102
+ static ɵfac: i0.ɵɵFactoryDeclaration<RhAuthService, never>;
103
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
104
+ }
105
+ /**
106
+ * Subscription, billing and order management.
107
+ *
108
+ * Separated from {@link RhAuthService} because payments are not authentication.
109
+ * Reads the current user from {@link RhAuthService} to derive
110
+ * `canManageBilling`, but owns its own subscription state.
111
+ *
112
+ * Subscription is loaded automatically when the user signs in and reloaded
113
+ * when they switch team (via an `effect` watching {@link teamKey}). No manual
114
+ * wiring needed.
115
+ *
116
+ * Only active when `config.payments` is `true` — otherwise every method
117
+ * is a no-op and no `/stripe/*` call is ever made.
118
+ */
119
+ export declare class RhPaymentsService {
120
+ private readonly config;
121
+ private readonly auth;
122
+ private readonly _subscription;
123
+ readonly subscription: import("@angular/core").Signal<Subscription | null>;
124
+ readonly plan: import("@angular/core").Signal<string | null>;
125
+ readonly isSubscribed: import("@angular/core").Signal<boolean>;
126
+ readonly canManageBilling: import("@angular/core").Signal<boolean>;
127
+ readonly seatsAvailable: import("@angular/core").Signal<number | null>;
128
+ /**
129
+ * The team the subscription belongs to: `null` when signed out, `''` for a
130
+ * signed-in user with no team.
131
+ *
132
+ * The effect below tracks this rather than `auth.user()` itself. Both
133
+ * `updateProfile` and `acceptConsents` re-run `checkSession`, which replaces
134
+ * the user with a freshly parsed document — tracking that object's identity
135
+ * would reload the subscription on every profile edit and consent
136
+ * acceptance, which the adapter contract rules out. A subscription changes
137
+ * with the team, not with the profile.
138
+ */
139
+ private readonly teamKey;
140
+ private get paymentsEnabled();
141
+ constructor();
142
+ /** Reload the team's subscription. No-op when `config.payments` is not `true`. */
143
+ loadSubscription(): Observable<Subscription | null>;
144
+ /** The service's subscription plan catalog. No session required. */
145
+ getPlans(): Observable<{
146
+ default_plan: string;
147
+ plans: Plan[];
148
+ }>;
149
+ /**
150
+ * Start a Stripe Checkout session.
151
+ *
152
+ * **Rejects with `status: 409`** when the team already has an active
153
+ * subscription — send them to {@link openBillingPortal} instead.
154
+ */
155
+ createCheckoutSession(plan: string, interval: 'month' | 'year'): Observable<{
156
+ url: string;
157
+ }>;
158
+ /** Open the Stripe Customer Portal for self-service plan changes. */
159
+ openBillingPortal(): Observable<{
160
+ url: string;
161
+ }>;
162
+ /** The team's seat licences. Requires `canManageBilling`. */
163
+ getLicenses(): Observable<Licenses>;
164
+ /**
165
+ * Grant a seat licence. Returns `'granted'` or `'already-licensed'`.
166
+ * Rejects with `status: 409` when no seat is available.
167
+ */
168
+ grantLicense(userId: string): Observable<GrantLicenseResult>;
169
+ /** Revoke a seat licence. */
170
+ revokeLicense(userId: string): Observable<void>;
171
+ /** Read the product catalog. */
172
+ getCatalog(opts?: CatalogQuery): Observable<CatalogItem[]>;
173
+ /**
174
+ * Create an order and start Checkout.
175
+ *
176
+ * @param email Required for guest checkout.
177
+ */
178
+ createOrder(items: {
179
+ productId: string;
180
+ quantity: number;
181
+ }[], email?: string, collection?: string): Observable<{
182
+ _id: {
183
+ $oid: string;
184
+ };
185
+ checkout_url: string;
186
+ secret: string;
187
+ }>;
188
+ /** Read an order back. */
189
+ getOrder(id: string, secret?: string, collection?: string): Observable<Order>;
190
+ /**
191
+ * Poll until the subscription satisfies `predicate`.
192
+ *
193
+ * Use on the Checkout success page — the redirect back from Stripe races
194
+ * the webhook, so a bare `getSubscription` can still show the old plan.
195
+ *
196
+ * **Timing out is not a payment failure.** Rejects with
197
+ * `WaitTimeoutError` when the webhook hasn't arrived yet.
198
+ */
199
+ waitForSubscription(predicate: (subscription: Subscription) => boolean, opts?: WaitOptions): Observable<Subscription>;
200
+ /**
201
+ * Poll until the order leaves `'pending_payment'`.
202
+ *
203
+ * Same race as {@link waitForSubscription} — use on the order success page.
204
+ *
205
+ * **Timing out is not a payment failure.** Rejects with
206
+ * `WaitTimeoutError` when the webhook hasn't arrived yet.
207
+ */
208
+ waitForOrder(id: string, secret?: string, opts?: WaitOptions & {
209
+ collection?: string;
210
+ }): Observable<Order>;
211
+ static ɵfac: i0.ɵɵFactoryDeclaration<RhPaymentsService, never>;
212
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
213
+ }
214
+ /**
215
+ * Where {@link RhCartService} keeps the cart in `localStorage`. Defaults to `'rh-cart'`.
216
+ *
217
+ * Worth providing when two of your apps share an origin, which is one deployment decision away
218
+ * from happening by accident.
219
+ *
220
+ * Declared here rather than in `tokens.ts`: that file is the authentication surface, and a cart
221
+ * needs no session. Importing the kit into it to reach one constant also dragged the kit's module
222
+ * into every spec that touches `RH_AUTH_CONFIG`, where it is auto-mocked — which shifted module
223
+ * initialisation enough to make two unrelated guard tests fail, in CI only.
224
+ */
225
+ export declare const RH_CART_STORAGE_KEY: InjectionToken<string>;
226
+ /**
227
+ * The shopping cart.
228
+ *
229
+ * Independent of {@link import('./auth.service.js').RhAuthService | RhAuthService}
230
+ * and of `RH_AUTH_CONFIG`: a cart belongs to the browser, not to a session, and
231
+ * a shop that makes people sign in before they can put something in a basket
232
+ * loses most of them there. It becomes an order — which does need a config —
233
+ * when {@link RhCartService.orderItems} is handed to `createOrder`.
234
+ *
235
+ * On the server, `localStorage` does not exist and the cart reads as empty.
236
+ * That is correct for a rendered page — a request carries no basket — but it
237
+ * does mean a server-rendered cart count starts at zero and fills in once the
238
+ * browser takes over.
239
+ */
240
+ export declare class RhCartService {
241
+ private readonly storageKey;
242
+ private readonly _lines;
243
+ /** The lines, in the order they were added. */
244
+ readonly lines: import("@angular/core").Signal<CartLine[]>;
245
+ private readonly totals;
246
+ /** Units, not lines: two of one thing counts two. */
247
+ readonly totalItems: import("@angular/core").Signal<number>;
248
+ /** Display only. Minor units, meaningful when every line shares a currency. */
249
+ readonly subtotal: import("@angular/core").Signal<number>;
250
+ /** The first line's currency, or `'eur'` when empty. */
251
+ readonly currency: import("@angular/core").Signal<string>;
252
+ /** The cart as `createOrder` wants it. */
253
+ readonly orderItems: import("@angular/core").Signal<import("@ulabase/kit").OrderItem[]>;
254
+ /** Adds an item, or increases the line already holding it. */
255
+ add(item: CartItem, quantity?: number): void;
256
+ /** Sets a line's quantity. Zero removes it. */
257
+ setQuantity(productId: string, quantity: number): void;
258
+ remove(productId: string): void;
259
+ clear(): void;
260
+ /**
261
+ * State and storage move together, in the operation that changed them.
262
+ *
263
+ * Deliberately not an `effect` reacting to the signal: an effect runs after
264
+ * the fact and can be skipped or reordered, and the failure that produces is
265
+ * a line coming back from storage after somebody removed it.
266
+ */
267
+ private apply;
268
+ static ɵfac: i0.ɵɵFactoryDeclaration<RhCartService, never>;
269
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
270
+ }
271
+ export declare const authGuard: CanActivateFn;
272
+ export declare const publicGuard: CanActivateFn;
273
+ /**
274
+ * Authenticates the application's own `HttpClient` requests, and clears the
275
+ * session when the service rejects one with a 401.
276
+ *
277
+ * The kit's own calls go out through `fetch` and never reach an Angular
278
+ * interceptor, so without this an app querying its own collections had to
279
+ * attach the bearer token by hand at every call site.
280
+ *
281
+ * **Only requests to `apiBaseUrl` are touched.** The token is a credential:
282
+ * attaching it to every outgoing request would hand it to any third-party host
283
+ * the app happens to call. Requests elsewhere pass through untouched — they
284
+ * still get the 401 handling, which is about the session and not the target.
285
+ *
286
+ * An `Authorization` header already on the request is left alone, so a call
287
+ * that needs different credentials can say so.
288
+ */
289
+ export declare const rhAuthInterceptor: HttpInterceptorFn;
290
+ /**
291
+ * A {@link AuthConfig.transport} backed by Angular's `HttpClient`, so the kit's
292
+ * own calls go through the interceptor chain like everything else the
293
+ * application sends.
294
+ *
295
+ * Without it the kit speaks `fetch` directly and an interceptor never sees a
296
+ * login, a session check or a token renewal — which makes any cross-cutting
297
+ * concern written as an interceptor quietly partial.
298
+ *
299
+ * Two mismatches with `fetch` are reconciled here:
300
+ *
301
+ * - **`HttpClient` throws on a non-2xx response.** `fetch` resolves and lets
302
+ * the caller read the status, which is what the core does to build its
303
+ * `ApiError`. So an `HttpErrorResponse` carrying a real response is turned
304
+ * back into a resolved `Response`.
305
+ * - **`status === 0` is not a response**, it is a network or CORS failure with
306
+ * nothing behind it. `new Response` would reject the status anyway, so the
307
+ * original error is rethrown and surfaces as a rejected promise, exactly as
308
+ * a failed `fetch` would.
309
+ *
310
+ * The body is read as text and left unparsed: the core calls `.json()` itself,
311
+ * and on an error response it needs the raw body to pull out the message.
312
+ */
313
+ export declare function httpClientTransport(http: HttpClient): (url: string, init?: RequestInit) => Promise<Response>;
314
+ export declare function provideRhAuth(config: AuthConfig): EnvironmentProviders;
315
+ export declare const RH_AUTH_CONFIG: InjectionToken<AuthConfig>;
316
+ /**
317
+ * Marks a request as originating from the kit rather than from application
318
+ * code. Set by `httpClientTransport`, read by `rhAuthInterceptor`.
319
+ *
320
+ * The kit owns the meaning of a 401 on its own endpoints, and that meaning is
321
+ * not always "the session is dead": `PATCH /auth/change-password` answers 401
322
+ * when the *current password* is wrong, and `GET /token` when the credentials
323
+ * are. Letting the interceptor clear the session on those would sign a user
324
+ * out for mistyping their old password.
325
+ */
326
+ declare const RH_KIT_REQUEST: HttpContextToken<boolean>;