@orechio/platform-angular 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Orechio
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,64 @@
1
+ # PlatformAngular
2
+
3
+ This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 22.1.0.
4
+
5
+ ## Code scaffolding
6
+
7
+ Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
8
+
9
+ ```bash
10
+ ng generate component component-name
11
+ ```
12
+
13
+ For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
14
+
15
+ ```bash
16
+ ng generate --help
17
+ ```
18
+
19
+ ## Building
20
+
21
+ To build the library, run:
22
+
23
+ ```bash
24
+ ng build platform-angular
25
+ ```
26
+
27
+ This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
28
+
29
+ ### Publishing the Library
30
+
31
+ Once the project is built, you can publish your library by following these steps:
32
+
33
+ 1. Navigate to the `dist` directory:
34
+
35
+ ```bash
36
+ cd dist/platform-angular
37
+ ```
38
+
39
+ 2. Run the `npm publish` command to publish your library to the npm registry:
40
+ ```bash
41
+ npm publish
42
+ ```
43
+
44
+ ## Running unit tests
45
+
46
+ To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
47
+
48
+ ```bash
49
+ ng test
50
+ ```
51
+
52
+ ## Running end-to-end tests
53
+
54
+ For end-to-end (e2e) testing, run:
55
+
56
+ ```bash
57
+ ng e2e
58
+ ```
59
+
60
+ Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
61
+
62
+ ## Additional Resources
63
+
64
+ For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
@@ -0,0 +1,360 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, signal, computed, Injectable, inject, TemplateRef, ViewContainerRef, Input, Directive } from '@angular/core';
3
+ import { HttpClient, HttpErrorResponse, provideHttpClient, withXsrfConfiguration, withInterceptors } from '@angular/common/http';
4
+ import { map, tap, finalize, catchError, throwError, share, ReplaySubject, switchMap } from 'rxjs';
5
+ import { Router } from '@angular/router';
6
+
7
+ const PLATFORM_ANGULAR_CONFIG = new InjectionToken('PLATFORM_ANGULAR_CONFIG');
8
+ function providePlatformAngularConfig(config) {
9
+ return { provide: PLATFORM_ANGULAR_CONFIG, useValue: config };
10
+ }
11
+
12
+ /**
13
+ * Generic signal-based session store. Mirrors the pattern already proven
14
+ * independently by both consumers (Portal's AuthService.user, Store's
15
+ * EstadoDaSessao) — deliberately NOT a richer state machine (unknown /
16
+ * loading / error states): neither consumer needed one, and ADR-010 §2.2
17
+ * ("projete para extração, não extraia por antecipação") argues against
18
+ * building one speculatively.
19
+ *
20
+ * Parameterized over TUser so each product can extend SessionUser with its
21
+ * own fields without needing a package release.
22
+ */
23
+ class SessionStore {
24
+ state = signal(null, /* @ts-ignore */
25
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
26
+ user = this.state.asReadonly();
27
+ isAuthenticated = computed(() => this.state() !== null, /* @ts-ignore */
28
+ ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : /* istanbul ignore next */ []));
29
+ set(user) {
30
+ this.state.set(user);
31
+ }
32
+ clear() {
33
+ this.state.set(null);
34
+ }
35
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: SessionStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
36
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: SessionStore, providedIn: 'root' });
37
+ }
38
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: SessionStore, decorators: [{
39
+ type: Injectable,
40
+ args: [{ providedIn: 'root' }]
41
+ }] });
42
+
43
+ /**
44
+ * Thin BFF client for the auth contract: login / refresh / logout, and an
45
+ * optional session-read ("me") endpoint. Deliberately does NOT decode JWTs,
46
+ * store bearer tokens, or treat itself as a security authority — per
47
+ * ADR-010 §12 and this package's explicit non-goal (no JWT client SDK): the
48
+ * BFF/backend is the only place tokens live, this class only reflects
49
+ * whatever session-shaped JSON the BFF hands back.
50
+ *
51
+ * Endpoint paths are entirely config-driven (PLATFORM_ANGULAR_CONFIG) — this
52
+ * class contains zero hardcoded URLs, so it works unmodified against
53
+ * Portal's /api/auth/{login,me,refresh,logout} (envelope-wrapped
54
+ * `{ user: UserInfo }` responses, unwrapped via config.auth.unwrapUser) and
55
+ * Store's /api/auth/{login,refresh,logout} (no `me`, unwrapped responses)
56
+ * alike.
57
+ */
58
+ class AuthClient {
59
+ http = inject(HttpClient);
60
+ config = inject(PLATFORM_ANGULAR_CONFIG);
61
+ session = inject(SessionStore);
62
+ url(path) {
63
+ return `${this.config.apiBaseUrl}${path}`;
64
+ }
65
+ unwrap(body) {
66
+ const unwrapUser = this.config.auth.unwrapUser;
67
+ return (unwrapUser ? unwrapUser(body) : body);
68
+ }
69
+ login(credentials) {
70
+ return this.http.post(this.url(this.config.auth.login), credentials).pipe(map((body) => this.unwrap(body)), tap((user) => this.session.set(user)));
71
+ }
72
+ /**
73
+ * Reads the current session. Uses the configured `me` endpoint (GET) when
74
+ * present (Portal); falls back to `refresh` (POST) when it isn't (Store,
75
+ * which has no dedicated session-read endpoint and bootstraps via refresh).
76
+ */
77
+ initSession() {
78
+ if (this.config.auth.me) {
79
+ return this.http.get(this.url(this.config.auth.me)).pipe(map((body) => this.unwrap(body)), tap((user) => this.session.set(user)));
80
+ }
81
+ return this.refresh();
82
+ }
83
+ refresh() {
84
+ return this.http.post(this.url(this.config.auth.refresh), {}).pipe(map((body) => this.unwrap(body)), tap((user) => this.session.set(user)));
85
+ }
86
+ /**
87
+ * Clears the local session unconditionally — on success, on error, and on
88
+ * unsubscribe (`finalize`, not `tap`) — matching the behavior both
89
+ * consumers already converged on independently (Portal swallows the
90
+ * error before clearing; Store clears via `finalize` while still letting
91
+ * the error propagate). This does NOT swallow errors: callers that want
92
+ * Portal's "always succeed" UX compose their own `catchError` on top.
93
+ */
94
+ logout() {
95
+ return this.http
96
+ .post(this.url(this.config.auth.logout), {})
97
+ .pipe(finalize(() => this.session.clear()));
98
+ }
99
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: AuthClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
100
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: AuthClient, providedIn: 'root' });
101
+ }
102
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: AuthClient, decorators: [{
103
+ type: Injectable,
104
+ args: [{ providedIn: 'root' }]
105
+ }] });
106
+
107
+ /**
108
+ * INTERNAL. Not exported from public-api.ts — consumers never construct or configure this
109
+ * directly; it exists purely to back `sessionExpiryInterceptor`'s `'refresh-then-redirect'`
110
+ * strategy (WEB-0.2, ADR-016 §"WEB-0.2 note").
111
+ *
112
+ * Deduplicates concurrent `AuthClient.refresh()` calls that originate from the SAME Angular
113
+ * application context (one injector, i.e. one running app/tab). Before this existed, every
114
+ * 401'd request independently called `authClient.refresh()`, and since that method returns a
115
+ * cold Observable (`http.post(...)`, no multicasting), N simultaneous 401s produced N
116
+ * independent `POST /refresh` calls — proven by the characterization test in
117
+ * `provide-platform-angular-http.spec.ts`.
118
+ *
119
+ * This is defense-in-depth ONLY. The Platform already tolerates concurrent legitimate refresh
120
+ * rotations correctly and safely (PLATFORM-REFRESH-1/1A/1B, ADR-016's retry leeway) — this class
121
+ * exists to cut needless network traffic, cryptographic work, and rate-limit pressure on the
122
+ * Platform, not to be the thing that makes concurrent refreshes safe. It does not, and does not
123
+ * need to, coordinate across browser tabs/windows or other Angular application instances — see
124
+ * the "Multi-tab" note on `sessionExpiryInterceptor` for why that's explicitly out of scope here
125
+ * and handled server-side instead.
126
+ */
127
+ class RefreshCoordinator {
128
+ authClient = inject(AuthClient);
129
+ session = inject(SessionStore);
130
+ router = inject(Router);
131
+ config = inject(PLATFORM_ANGULAR_CONFIG);
132
+ inFlight$ = null;
133
+ /**
134
+ * Returns the single in-flight refresh operation if one is already running, or starts a new
135
+ * one. Every caller — first or piggybacking — gets the SAME underlying HTTP call's outcome.
136
+ *
137
+ * Semantics (WEB-0.2):
138
+ * - Single-flight, not a permanent cache: once the shared operation settles (success or
139
+ * error), the reference is cleared (`finalize`, placed BEFORE `share()` so it fires exactly
140
+ * once per underlying HTTP call, not once per subscriber — see the `share()` config note
141
+ * below). The next call to `refresh()` after that always starts a genuinely new request.
142
+ * - On failure, the session is cleared and the browser is navigated to the login route
143
+ * exactly once, regardless of how many callers are waiting — not once per waiting caller.
144
+ * Each caller still receives its own error notification (multicast by `share()`) and is
145
+ * responsible for propagating whatever error IT needs to its own subscriber (see
146
+ * `sessionExpiryInterceptor`, which re-throws the ORIGINAL 401 to the request that
147
+ * triggered it, not this refresh failure).
148
+ * - `resetOnRefCountZero: false` is load-bearing: if one waiting caller unsubscribes (e.g. a
149
+ * cancelled navigation), the still-in-flight HTTP request for the OTHER callers must not be
150
+ * aborted. Angular's `HttpClient` cancels the underlying network request when its Observable
151
+ * loses its last subscriber — without this flag, one caller going away could silently
152
+ * cancel the refresh everyone else is still waiting on.
153
+ * - `resetOnError`/`resetOnComplete` are left `false` too: this class never reuses a settled
154
+ * `inFlight$` reference across cycles (the field is always nulled and rebuilt from scratch
155
+ * by `finalize`), so `share()` itself never needs to reset its own internal connector.
156
+ */
157
+ refresh() {
158
+ if (this.inFlight$) {
159
+ return this.inFlight$;
160
+ }
161
+ this.inFlight$ = this.authClient.refresh().pipe(catchError((error) => {
162
+ this.session.clear();
163
+ void this.router.navigateByUrl(this.config.loginRoute ?? '/login');
164
+ return throwError(() => error);
165
+ }), finalize(() => {
166
+ this.inFlight$ = null;
167
+ }), share({
168
+ connector: () => new ReplaySubject(1),
169
+ resetOnError: false,
170
+ resetOnComplete: false,
171
+ resetOnRefCountZero: false,
172
+ }));
173
+ return this.inFlight$;
174
+ }
175
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: RefreshCoordinator, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
176
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: RefreshCoordinator, providedIn: 'root' });
177
+ }
178
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: RefreshCoordinator, decorators: [{
179
+ type: Injectable,
180
+ args: [{ providedIn: 'root' }]
181
+ }] });
182
+
183
+ function isApiRequest(url, apiBaseUrl) {
184
+ return url === apiBaseUrl || url.startsWith(`${apiBaseUrl}/`);
185
+ }
186
+ function isAuthEndpoint(url, apiBaseUrl, auth) {
187
+ const authPaths = [auth.login, auth.refresh, auth.logout, auth.me].filter((p) => !!p);
188
+ return authPaths.some((p) => url === `${apiBaseUrl}${p}`);
189
+ }
190
+ /**
191
+ * Forces `withCredentials: true` on same-origin API requests, so the BFF's
192
+ * HttpOnly session cookie is sent. Proven identically in both consumers.
193
+ * XSRF header injection is intentionally NOT duplicated here — Angular's
194
+ * own `withXsrfConfiguration` (registered by `providePlatformAngularHttp`)
195
+ * already does that. Portal's local interceptor previously re-implemented
196
+ * XSRF header injection manually alongside `withXsrfConfiguration`; that
197
+ * was redundant, not a pattern to preserve.
198
+ */
199
+ const credentialsInterceptor = (req, next) => {
200
+ const { apiBaseUrl } = inject(PLATFORM_ANGULAR_CONFIG);
201
+ if (!isApiRequest(req.url, apiBaseUrl)) {
202
+ return next(req);
203
+ }
204
+ return next(req.clone({ withCredentials: true }));
205
+ };
206
+ /**
207
+ * On 401 from a non-auth API request: 'redirect' (default) clears the
208
+ * session and navigates to the login route immediately (Store's proven
209
+ * behavior). 'refresh-then-redirect' attempts one silent refresh first and
210
+ * retries the original request on success, falling back to redirect
211
+ * (Portal's proven behavior). Configured via
212
+ * PlatformAngularConfig.unauthorizedStrategy.
213
+ *
214
+ * WEB-0.2: the 'refresh-then-redirect' path routes through `RefreshCoordinator`
215
+ * instead of calling `AuthClient.refresh()` directly — every 401 handled by
216
+ * the SAME running app shares one in-flight refresh instead of each request
217
+ * triggering its own. The 'redirect' strategy is untouched: it never calls
218
+ * refresh at all, so there is nothing to coordinate (see the config note on
219
+ * `PlatformAngularConfig.unauthorizedStrategy`). On refresh failure, the
220
+ * session-clear/navigate side effect now happens once inside the coordinator
221
+ * (shared across every waiting caller) rather than once per waiting caller;
222
+ * each caller still receives the ORIGINAL 401 error, not a refresh-specific
223
+ * one, exactly as before. Multi-tab/multi-window coordination is explicitly
224
+ * NOT handled here — see `RefreshCoordinator`'s doc comment — the Platform's
225
+ * own server-side retry leeway (ADR-016) is what makes concurrent refreshes
226
+ * from independent contexts safe.
227
+ */
228
+ const sessionExpiryInterceptor = (req, next) => {
229
+ const config = inject(PLATFORM_ANGULAR_CONFIG);
230
+ const session = inject(SessionStore);
231
+ const router = inject(Router);
232
+ const refreshCoordinator = inject(RefreshCoordinator);
233
+ if (!isApiRequest(req.url, config.apiBaseUrl) ||
234
+ isAuthEndpoint(req.url, config.apiBaseUrl, config.auth)) {
235
+ return next(req);
236
+ }
237
+ return next(req).pipe(catchError((error) => {
238
+ if (!(error instanceof HttpErrorResponse) || error.status !== 401) {
239
+ return throwError(() => error);
240
+ }
241
+ if (config.unauthorizedStrategy !== 'refresh-then-redirect') {
242
+ session.clear();
243
+ void router.navigateByUrl(config.loginRoute ?? '/login');
244
+ return throwError(() => error);
245
+ }
246
+ return refreshCoordinator.refresh().pipe(switchMap(() => next(req)),
247
+ // The coordinator already cleared the session and navigated once for
248
+ // this shared failure (see RefreshCoordinator.refresh()) — this
249
+ // request still needs to fail with its OWN original 401, not the
250
+ // refresh's error, exactly as before WEB-0.2.
251
+ catchError(() => throwError(() => error)));
252
+ }));
253
+ };
254
+ /**
255
+ * Bundles provideHttpClient with the XSRF configuration and interceptors
256
+ * proven necessary by both consumers. Cookie/header names default to
257
+ * 'XSRF-TOKEN'/'X-XSRF-TOKEN' (identical in both consumers today) but
258
+ * remain overridable via config. Consumers add their own extra
259
+ * provideHttpClient features (e.g. Portal's withXhr()) by calling
260
+ * provideHttpClient(...) separately — this function only owns the concerns
261
+ * proven common to both.
262
+ */
263
+ function providePlatformAngularHttp(...extraFeatures) {
264
+ return provideHttpClient(withXsrfConfiguration({
265
+ cookieName: 'XSRF-TOKEN',
266
+ headerName: 'X-XSRF-TOKEN',
267
+ }), withInterceptors([credentialsInterceptor, sessionExpiryInterceptor]), ...extraFeatures);
268
+ }
269
+
270
+ function hasCapability(capabilities, key) {
271
+ return capabilities?.[key] === true;
272
+ }
273
+
274
+ /**
275
+ * Structural directive gating template content on a capability key from a
276
+ * consumer-supplied CapabilityMap. Net-new (neither consumer had factored
277
+ * their proven inline `@if (capabilities()?.x)` pattern into a directive),
278
+ * but grounded in Portal's validated usage — see capability.model.ts.
279
+ *
280
+ * UX ONLY — see capability.model.ts. Hiding an element here does not
281
+ * authorize anything; the backend re-validates on every mutating call.
282
+ *
283
+ * Usage: `<button *orxCapability="capabilities(); has: 'manageTenants'">`
284
+ */
285
+ class CapabilityDirective {
286
+ templateRef = inject((TemplateRef));
287
+ viewContainer = inject(ViewContainerRef);
288
+ map;
289
+ key;
290
+ hasView = false;
291
+ set orxCapability(map) {
292
+ this.map = map;
293
+ this.render();
294
+ }
295
+ set has(key) {
296
+ this.key = key;
297
+ this.render();
298
+ }
299
+ render() {
300
+ const shouldShow = !!this.key && hasCapability(this.map, this.key);
301
+ if (shouldShow && !this.hasView) {
302
+ this.viewContainer.createEmbeddedView(this.templateRef);
303
+ this.hasView = true;
304
+ }
305
+ else if (!shouldShow && this.hasView) {
306
+ this.viewContainer.clear();
307
+ this.hasView = false;
308
+ }
309
+ }
310
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: CapabilityDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
311
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.2", type: CapabilityDirective, isStandalone: true, selector: "[orxCapability]", inputs: { orxCapability: "orxCapability", has: ["orxCapabilityHas", "has"] }, ngImport: i0 });
312
+ }
313
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: CapabilityDirective, decorators: [{
314
+ type: Directive,
315
+ args: [{
316
+ selector: '[orxCapability]',
317
+ standalone: true,
318
+ }]
319
+ }], propDecorators: { orxCapability: [{
320
+ type: Input
321
+ }], has: [{
322
+ type: Input,
323
+ args: ['orxCapabilityHas']
324
+ }] } });
325
+
326
+ /**
327
+ * Normalizes an HttpErrorResponse into ProblemDetails. Mirrors Store's
328
+ * normalizarErroApi exactly, generalized to make `message` optional and to
329
+ * accept an HttpErrorResponse without any consumer-specific fallback
330
+ * message map (each product keeps its own code→message copy locally —
331
+ * that's product-specific text, not shared infrastructure).
332
+ */
333
+ function normalizeProblemDetails(error) {
334
+ const body = error.error;
335
+ const code = typeof body?.['code'] === 'string' ? body['code'] : `http.${error.status || 0}`;
336
+ const message = typeof body?.['message'] === 'string' ? body['message'] : undefined;
337
+ const required = typeof body?.['required'] === 'string' ? body['required'] : undefined;
338
+ // Wire field is `correlation_id` (snake_case, ADR-017 §3) — kept as-is on the wire,
339
+ // exposed as `correlationId` here per TS/Angular convention (same server-side pattern:
340
+ // OrechioProblemDetails.CorrelationId is [JsonPropertyName("correlation_id")]).
341
+ const correlationId = typeof body?.['correlation_id'] === 'string' ? body['correlation_id'] : undefined;
342
+ return {
343
+ code,
344
+ message,
345
+ status: error.status,
346
+ ...(required ? { required } : {}),
347
+ ...(correlationId ? { correlationId } : {}),
348
+ };
349
+ }
350
+
351
+ /*
352
+ * Public API Surface of @orechio/platform-angular
353
+ */
354
+
355
+ /**
356
+ * Generated bundle index. Do not edit.
357
+ */
358
+
359
+ export { AuthClient, CapabilityDirective, PLATFORM_ANGULAR_CONFIG, SessionStore, credentialsInterceptor, hasCapability, normalizeProblemDetails, providePlatformAngularConfig, providePlatformAngularHttp, sessionExpiryInterceptor };
360
+ //# sourceMappingURL=orechio-platform-angular.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orechio-platform-angular.mjs","sources":["../../../projects/platform-angular/src/lib/config/platform-angular-config.ts","../../../projects/platform-angular/src/lib/session/session-store.ts","../../../projects/platform-angular/src/lib/auth/auth-client.ts","../../../projects/platform-angular/src/lib/auth/refresh-coordinator.ts","../../../projects/platform-angular/src/lib/http/provide-platform-angular-http.ts","../../../projects/platform-angular/src/lib/capabilities/capability.model.ts","../../../projects/platform-angular/src/lib/capabilities/capability.directive.ts","../../../projects/platform-angular/src/lib/problem-details/problem-details.model.ts","../../../projects/platform-angular/src/public-api.ts","../../../projects/platform-angular/src/orechio-platform-angular.ts"],"sourcesContent":["import { InjectionToken, Provider } from '@angular/core';\r\n\r\n/**\r\n * BFF endpoint paths for the auth contract. All paths are relative to `apiBaseUrl`.\r\n * `me` is optional: some BFFs bootstrap the session via `refresh` alone (no dedicated\r\n * session-read endpoint) instead of a separate `me` call.\r\n */\r\nexport interface PlatformAngularAuthPaths {\r\n login: string;\r\n refresh: string;\r\n logout: string;\r\n me?: string;\r\n /**\r\n * Unwraps the raw auth-endpoint response body into TUser. Defaults to the\r\n * identity function (response body IS the user object), matching Store's\r\n * proven wire format. Portal's BFF wraps the user in an envelope\r\n * (`{ user: UserInfo }`) — Portal supplies `(body) => body.user` here\r\n * rather than the package assuming either shape.\r\n */\r\n unwrapUser?: (body: unknown) => unknown;\r\n}\r\n\r\nexport interface PlatformAngularConfig {\r\n /** Same-origin API base path, e.g. '/api'. Never an absolute cross-origin URL — the\r\n * BFF/cookie-session pattern this library assumes requires same-origin requests. */\r\n apiBaseUrl: string;\r\n auth: PlatformAngularAuthPaths;\r\n /** XSRF cookie/header names. Defaults match Angular's own defaults and both proven\r\n * consumers (Portal, Store): cookie 'XSRF-TOKEN', header 'X-XSRF-TOKEN'. */\r\n xsrf?: {\r\n cookieName?: string;\r\n headerName?: string;\r\n };\r\n /** 401 handling strategy. 'redirect' clears the session and redirects immediately\r\n * (Store's proven behavior). 'refresh-then-redirect' attempts one silent refresh\r\n * before giving up (Portal's proven behavior). Defaults to 'redirect' — the simpler,\r\n * more conservative behavior — when omitted. */\r\n unauthorizedStrategy?: 'redirect' | 'refresh-then-redirect';\r\n /** Route to navigate to after a session is cleared. Defaults to '/login'. */\r\n loginRoute?: string;\r\n}\r\n\r\nexport const PLATFORM_ANGULAR_CONFIG = new InjectionToken<PlatformAngularConfig>(\r\n 'PLATFORM_ANGULAR_CONFIG',\r\n);\r\n\r\nexport function providePlatformAngularConfig(config: PlatformAngularConfig): Provider {\r\n return { provide: PLATFORM_ANGULAR_CONFIG, useValue: config };\r\n}\r\n","import { Injectable, Signal, computed, signal } from '@angular/core';\r\nimport { SessionUser } from './session-user.model';\r\n\r\n/**\r\n * Generic signal-based session store. Mirrors the pattern already proven\r\n * independently by both consumers (Portal's AuthService.user, Store's\r\n * EstadoDaSessao) — deliberately NOT a richer state machine (unknown /\r\n * loading / error states): neither consumer needed one, and ADR-010 §2.2\r\n * (\"projete para extração, não extraia por antecipação\") argues against\r\n * building one speculatively.\r\n *\r\n * Parameterized over TUser so each product can extend SessionUser with its\r\n * own fields without needing a package release.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class SessionStore<TUser extends SessionUser = SessionUser> {\r\n private readonly state = signal<TUser | null>(null);\r\n\r\n readonly user: Signal<TUser | null> = this.state.asReadonly();\r\n readonly isAuthenticated: Signal<boolean> = computed(() => this.state() !== null);\r\n\r\n set(user: TUser): void {\r\n this.state.set(user);\r\n }\r\n\r\n clear(): void {\r\n this.state.set(null);\r\n }\r\n}\r\n","import { HttpClient } from '@angular/common/http';\r\nimport { Injectable, inject } from '@angular/core';\r\nimport { Observable, finalize, map, tap } from 'rxjs';\r\nimport { PLATFORM_ANGULAR_CONFIG } from '../config/platform-angular-config';\r\nimport { SessionStore } from '../session/session-store';\r\nimport { SessionUser } from '../session/session-user.model';\r\n\r\n/**\r\n * Thin BFF client for the auth contract: login / refresh / logout, and an\r\n * optional session-read (\"me\") endpoint. Deliberately does NOT decode JWTs,\r\n * store bearer tokens, or treat itself as a security authority — per\r\n * ADR-010 §12 and this package's explicit non-goal (no JWT client SDK): the\r\n * BFF/backend is the only place tokens live, this class only reflects\r\n * whatever session-shaped JSON the BFF hands back.\r\n *\r\n * Endpoint paths are entirely config-driven (PLATFORM_ANGULAR_CONFIG) — this\r\n * class contains zero hardcoded URLs, so it works unmodified against\r\n * Portal's /api/auth/{login,me,refresh,logout} (envelope-wrapped\r\n * `{ user: UserInfo }` responses, unwrapped via config.auth.unwrapUser) and\r\n * Store's /api/auth/{login,refresh,logout} (no `me`, unwrapped responses)\r\n * alike.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class AuthClient<TUser extends SessionUser = SessionUser> {\r\n private readonly http = inject(HttpClient);\r\n private readonly config = inject(PLATFORM_ANGULAR_CONFIG);\r\n private readonly session = inject(SessionStore) as SessionStore<TUser>;\r\n\r\n private url(path: string): string {\r\n return `${this.config.apiBaseUrl}${path}`;\r\n }\r\n\r\n private unwrap(body: unknown): TUser {\r\n const unwrapUser = this.config.auth.unwrapUser;\r\n return (unwrapUser ? unwrapUser(body) : body) as TUser;\r\n }\r\n\r\n login(credentials: unknown): Observable<TUser> {\r\n return this.http.post<unknown>(this.url(this.config.auth.login), credentials).pipe(\r\n map((body) => this.unwrap(body)),\r\n tap((user) => this.session.set(user)),\r\n );\r\n }\r\n\r\n /**\r\n * Reads the current session. Uses the configured `me` endpoint (GET) when\r\n * present (Portal); falls back to `refresh` (POST) when it isn't (Store,\r\n * which has no dedicated session-read endpoint and bootstraps via refresh).\r\n */\r\n initSession(): Observable<TUser> {\r\n if (this.config.auth.me) {\r\n return this.http.get<unknown>(this.url(this.config.auth.me)).pipe(\r\n map((body) => this.unwrap(body)),\r\n tap((user) => this.session.set(user)),\r\n );\r\n }\r\n return this.refresh();\r\n }\r\n\r\n refresh(): Observable<TUser> {\r\n return this.http.post<unknown>(this.url(this.config.auth.refresh), {}).pipe(\r\n map((body) => this.unwrap(body)),\r\n tap((user) => this.session.set(user)),\r\n );\r\n }\r\n\r\n /**\r\n * Clears the local session unconditionally — on success, on error, and on\r\n * unsubscribe (`finalize`, not `tap`) — matching the behavior both\r\n * consumers already converged on independently (Portal swallows the\r\n * error before clearing; Store clears via `finalize` while still letting\r\n * the error propagate). This does NOT swallow errors: callers that want\r\n * Portal's \"always succeed\" UX compose their own `catchError` on top.\r\n */\r\n logout(): Observable<void> {\r\n return this.http\r\n .post<void>(this.url(this.config.auth.logout), {})\r\n .pipe(finalize(() => this.session.clear()));\r\n }\r\n}\r\n","import { inject, Injectable } from '@angular/core';\r\nimport { Router } from '@angular/router';\r\nimport { Observable, ReplaySubject, catchError, finalize, share, throwError } from 'rxjs';\r\nimport { PLATFORM_ANGULAR_CONFIG } from '../config/platform-angular-config';\r\nimport { SessionStore } from '../session/session-store';\r\nimport { SessionUser } from '../session/session-user.model';\r\nimport { AuthClient } from './auth-client';\r\n\r\n/**\r\n * INTERNAL. Not exported from public-api.ts — consumers never construct or configure this\r\n * directly; it exists purely to back `sessionExpiryInterceptor`'s `'refresh-then-redirect'`\r\n * strategy (WEB-0.2, ADR-016 §\"WEB-0.2 note\").\r\n *\r\n * Deduplicates concurrent `AuthClient.refresh()` calls that originate from the SAME Angular\r\n * application context (one injector, i.e. one running app/tab). Before this existed, every\r\n * 401'd request independently called `authClient.refresh()`, and since that method returns a\r\n * cold Observable (`http.post(...)`, no multicasting), N simultaneous 401s produced N\r\n * independent `POST /refresh` calls — proven by the characterization test in\r\n * `provide-platform-angular-http.spec.ts`.\r\n *\r\n * This is defense-in-depth ONLY. The Platform already tolerates concurrent legitimate refresh\r\n * rotations correctly and safely (PLATFORM-REFRESH-1/1A/1B, ADR-016's retry leeway) — this class\r\n * exists to cut needless network traffic, cryptographic work, and rate-limit pressure on the\r\n * Platform, not to be the thing that makes concurrent refreshes safe. It does not, and does not\r\n * need to, coordinate across browser tabs/windows or other Angular application instances — see\r\n * the \"Multi-tab\" note on `sessionExpiryInterceptor` for why that's explicitly out of scope here\r\n * and handled server-side instead.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class RefreshCoordinator {\r\n private readonly authClient = inject(AuthClient);\r\n private readonly session = inject(SessionStore);\r\n private readonly router = inject(Router);\r\n private readonly config = inject(PLATFORM_ANGULAR_CONFIG);\r\n\r\n private inFlight$: Observable<SessionUser> | null = null;\r\n\r\n /**\r\n * Returns the single in-flight refresh operation if one is already running, or starts a new\r\n * one. Every caller — first or piggybacking — gets the SAME underlying HTTP call's outcome.\r\n *\r\n * Semantics (WEB-0.2):\r\n * - Single-flight, not a permanent cache: once the shared operation settles (success or\r\n * error), the reference is cleared (`finalize`, placed BEFORE `share()` so it fires exactly\r\n * once per underlying HTTP call, not once per subscriber — see the `share()` config note\r\n * below). The next call to `refresh()` after that always starts a genuinely new request.\r\n * - On failure, the session is cleared and the browser is navigated to the login route\r\n * exactly once, regardless of how many callers are waiting — not once per waiting caller.\r\n * Each caller still receives its own error notification (multicast by `share()`) and is\r\n * responsible for propagating whatever error IT needs to its own subscriber (see\r\n * `sessionExpiryInterceptor`, which re-throws the ORIGINAL 401 to the request that\r\n * triggered it, not this refresh failure).\r\n * - `resetOnRefCountZero: false` is load-bearing: if one waiting caller unsubscribes (e.g. a\r\n * cancelled navigation), the still-in-flight HTTP request for the OTHER callers must not be\r\n * aborted. Angular's `HttpClient` cancels the underlying network request when its Observable\r\n * loses its last subscriber — without this flag, one caller going away could silently\r\n * cancel the refresh everyone else is still waiting on.\r\n * - `resetOnError`/`resetOnComplete` are left `false` too: this class never reuses a settled\r\n * `inFlight$` reference across cycles (the field is always nulled and rebuilt from scratch\r\n * by `finalize`), so `share()` itself never needs to reset its own internal connector.\r\n */\r\n refresh(): Observable<SessionUser> {\r\n if (this.inFlight$) {\r\n return this.inFlight$;\r\n }\r\n\r\n this.inFlight$ = this.authClient.refresh().pipe(\r\n catchError((error: unknown) => {\r\n this.session.clear();\r\n void this.router.navigateByUrl(this.config.loginRoute ?? '/login');\r\n return throwError(() => error);\r\n }),\r\n finalize(() => {\r\n this.inFlight$ = null;\r\n }),\r\n share({\r\n connector: () => new ReplaySubject<SessionUser>(1),\r\n resetOnError: false,\r\n resetOnComplete: false,\r\n resetOnRefCountZero: false,\r\n }),\r\n );\r\n\r\n return this.inFlight$;\r\n }\r\n}\r\n","import {\r\n HttpErrorResponse,\r\n HttpInterceptorFn,\r\n provideHttpClient,\r\n withInterceptors,\r\n withXsrfConfiguration,\r\n HttpFeature,\r\n HttpFeatureKind,\r\n} from '@angular/common/http';\r\nimport { inject } from '@angular/core';\r\nimport { Router } from '@angular/router';\r\nimport { catchError, switchMap, throwError } from 'rxjs';\r\nimport { PLATFORM_ANGULAR_CONFIG, PlatformAngularAuthPaths } from '../config/platform-angular-config';\r\nimport { RefreshCoordinator } from '../auth/refresh-coordinator';\r\nimport { SessionStore } from '../session/session-store';\r\n\r\nfunction isApiRequest(url: string, apiBaseUrl: string): boolean {\r\n return url === apiBaseUrl || url.startsWith(`${apiBaseUrl}/`);\r\n}\r\n\r\nfunction isAuthEndpoint(url: string, apiBaseUrl: string, auth: PlatformAngularAuthPaths): boolean {\r\n const authPaths = [auth.login, auth.refresh, auth.logout, auth.me].filter(\r\n (p): p is string => !!p,\r\n );\r\n return authPaths.some((p) => url === `${apiBaseUrl}${p}`);\r\n}\r\n\r\n/**\r\n * Forces `withCredentials: true` on same-origin API requests, so the BFF's\r\n * HttpOnly session cookie is sent. Proven identically in both consumers.\r\n * XSRF header injection is intentionally NOT duplicated here — Angular's\r\n * own `withXsrfConfiguration` (registered by `providePlatformAngularHttp`)\r\n * already does that. Portal's local interceptor previously re-implemented\r\n * XSRF header injection manually alongside `withXsrfConfiguration`; that\r\n * was redundant, not a pattern to preserve.\r\n */\r\nexport const credentialsInterceptor: HttpInterceptorFn = (req, next) => {\r\n const { apiBaseUrl } = inject(PLATFORM_ANGULAR_CONFIG);\r\n if (!isApiRequest(req.url, apiBaseUrl)) {\r\n return next(req);\r\n }\r\n return next(req.clone({ withCredentials: true }));\r\n};\r\n\r\n/**\r\n * On 401 from a non-auth API request: 'redirect' (default) clears the\r\n * session and navigates to the login route immediately (Store's proven\r\n * behavior). 'refresh-then-redirect' attempts one silent refresh first and\r\n * retries the original request on success, falling back to redirect\r\n * (Portal's proven behavior). Configured via\r\n * PlatformAngularConfig.unauthorizedStrategy.\r\n *\r\n * WEB-0.2: the 'refresh-then-redirect' path routes through `RefreshCoordinator`\r\n * instead of calling `AuthClient.refresh()` directly — every 401 handled by\r\n * the SAME running app shares one in-flight refresh instead of each request\r\n * triggering its own. The 'redirect' strategy is untouched: it never calls\r\n * refresh at all, so there is nothing to coordinate (see the config note on\r\n * `PlatformAngularConfig.unauthorizedStrategy`). On refresh failure, the\r\n * session-clear/navigate side effect now happens once inside the coordinator\r\n * (shared across every waiting caller) rather than once per waiting caller;\r\n * each caller still receives the ORIGINAL 401 error, not a refresh-specific\r\n * one, exactly as before. Multi-tab/multi-window coordination is explicitly\r\n * NOT handled here — see `RefreshCoordinator`'s doc comment — the Platform's\r\n * own server-side retry leeway (ADR-016) is what makes concurrent refreshes\r\n * from independent contexts safe.\r\n */\r\nexport const sessionExpiryInterceptor: HttpInterceptorFn = (req, next) => {\r\n const config = inject(PLATFORM_ANGULAR_CONFIG);\r\n const session = inject(SessionStore);\r\n const router = inject(Router);\r\n const refreshCoordinator = inject(RefreshCoordinator);\r\n\r\n if (\r\n !isApiRequest(req.url, config.apiBaseUrl) ||\r\n isAuthEndpoint(req.url, config.apiBaseUrl, config.auth)\r\n ) {\r\n return next(req);\r\n }\r\n\r\n return next(req).pipe(\r\n catchError((error: unknown) => {\r\n if (!(error instanceof HttpErrorResponse) || error.status !== 401) {\r\n return throwError(() => error);\r\n }\r\n\r\n if (config.unauthorizedStrategy !== 'refresh-then-redirect') {\r\n session.clear();\r\n void router.navigateByUrl(config.loginRoute ?? '/login');\r\n return throwError(() => error);\r\n }\r\n\r\n return refreshCoordinator.refresh().pipe(\r\n switchMap(() => next(req)),\r\n // The coordinator already cleared the session and navigated once for\r\n // this shared failure (see RefreshCoordinator.refresh()) — this\r\n // request still needs to fail with its OWN original 401, not the\r\n // refresh's error, exactly as before WEB-0.2.\r\n catchError(() => throwError(() => error)),\r\n );\r\n }),\r\n );\r\n};\r\n\r\n/**\r\n * Bundles provideHttpClient with the XSRF configuration and interceptors\r\n * proven necessary by both consumers. Cookie/header names default to\r\n * 'XSRF-TOKEN'/'X-XSRF-TOKEN' (identical in both consumers today) but\r\n * remain overridable via config. Consumers add their own extra\r\n * provideHttpClient features (e.g. Portal's withXhr()) by calling\r\n * provideHttpClient(...) separately — this function only owns the concerns\r\n * proven common to both.\r\n */\r\nexport function providePlatformAngularHttp(...extraFeatures: HttpFeature<HttpFeatureKind>[]) {\r\n return provideHttpClient(\r\n withXsrfConfiguration({\r\n cookieName: 'XSRF-TOKEN',\r\n headerName: 'X-XSRF-TOKEN',\r\n }),\r\n withInterceptors([credentialsInterceptor, sessionExpiryInterceptor]),\r\n ...extraFeatures,\r\n );\r\n}\r\n","/**\r\n * Generic capability map: a flat set of boolean feature flags fetched once\r\n * from the BFF (Portal's proven pattern — GET .../capabilities returning a\r\n * flat boolean-keyed object). Deliberately a generic `Record<string,\r\n * boolean>` rather than a typed interface enumerating every field: Portal's\r\n * capability set already has 17 product-specific fields and Store will\r\n * grow its own, unrelated set. A typed interface would require a package\r\n * release for every new product capability (see ADR-010 §18 — evolution\r\n * cadence mismatch is a real argument against sharing). Each product\r\n * defines its own capability keys locally and reads them through this map.\r\n *\r\n * UX ONLY. This does not replace server-side authorization. Every consumer\r\n * MUST continue to handle 403 from the actual mutating/reading endpoint —\r\n * capability checks here are a consultive pre-check, exactly as already\r\n * documented in Portal's application.service.ts before this extraction.\r\n */\r\nexport type CapabilityMap = Readonly<Record<string, boolean>>;\r\n\r\nexport function hasCapability(capabilities: CapabilityMap | null | undefined, key: string): boolean {\r\n return capabilities?.[key] === true;\r\n}\r\n","import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core';\r\nimport { CapabilityMap, hasCapability } from './capability.model';\r\n\r\n/**\r\n * Structural directive gating template content on a capability key from a\r\n * consumer-supplied CapabilityMap. Net-new (neither consumer had factored\r\n * their proven inline `@if (capabilities()?.x)` pattern into a directive),\r\n * but grounded in Portal's validated usage — see capability.model.ts.\r\n *\r\n * UX ONLY — see capability.model.ts. Hiding an element here does not\r\n * authorize anything; the backend re-validates on every mutating call.\r\n *\r\n * Usage: `<button *orxCapability=\"capabilities(); has: 'manageTenants'\">`\r\n */\r\n@Directive({\r\n selector: '[orxCapability]',\r\n standalone: true,\r\n})\r\nexport class CapabilityDirective {\r\n private readonly templateRef = inject(TemplateRef<unknown>);\r\n private readonly viewContainer = inject(ViewContainerRef);\r\n\r\n private map: CapabilityMap | null | undefined;\r\n private key: string | undefined;\r\n private hasView = false;\r\n\r\n @Input()\r\n set orxCapability(map: CapabilityMap | null | undefined) {\r\n this.map = map;\r\n this.render();\r\n }\r\n\r\n @Input('orxCapabilityHas')\r\n set has(key: string) {\r\n this.key = key;\r\n this.render();\r\n }\r\n\r\n private render(): void {\r\n const shouldShow = !!this.key && hasCapability(this.map, this.key);\r\n if (shouldShow && !this.hasView) {\r\n this.viewContainer.createEmbeddedView(this.templateRef);\r\n this.hasView = true;\r\n } else if (!shouldShow && this.hasView) {\r\n this.viewContainer.clear();\r\n this.hasView = false;\r\n }\r\n }\r\n}\r\n","import { HttpErrorResponse } from '@angular/common/http';\r\n\r\n/**\r\n * Canonical error shape, generalized from Store's already-proven\r\n * ErroApresentavel/normalizarErroApi (the more mature of the two consumers'\r\n * error handling — Portal had no typed contract at all, just five\r\n * duplicated `error.error.code` reads). `message` is optional: Portal never\r\n * received a server-supplied message field, only `code`, and resolves\r\n * user-facing text locally — so this contract cannot require it.\r\n *\r\n * `correlation_id` (API-1, ADR-017 §3.3): added after confirming directly\r\n * from Orechio.Platform's `ProblemResponses.cs` that it IS present in the\r\n * JSON body of every error response the Platform host produces (not just\r\n * the `X-Correlation-ID` response header), and that Portal's BFF relays\r\n * that body byte-for-byte, unreshaped, for every typed endpoint. The\r\n * earlier NG-1-era absence was accurate for what it tested at the time but\r\n * incomplete relative to Platform's actual wire contract. Optional: not\r\n * every error path in every consumer emits this field today (Store's own\r\n * narrower auth/CSRF error shapes don't) — its absence must not be treated\r\n * as a parse failure.\r\n *\r\n * Domain-specific error codes (e.g. 'access.platform_only',\r\n * 'tenant.not_found', 'csrf.invalid') are NOT enumerated here — `code`\r\n * stays a plain string so any code survives normalization unmodified and\r\n * remains identifiable by callers, per Passo 28.\r\n */\r\nexport interface ProblemDetails {\r\n code: string;\r\n message?: string;\r\n status: number;\r\n required?: string;\r\n correlationId?: string;\r\n}\r\n\r\n/**\r\n * Normalizes an HttpErrorResponse into ProblemDetails. Mirrors Store's\r\n * normalizarErroApi exactly, generalized to make `message` optional and to\r\n * accept an HttpErrorResponse without any consumer-specific fallback\r\n * message map (each product keeps its own code→message copy locally —\r\n * that's product-specific text, not shared infrastructure).\r\n */\r\nexport function normalizeProblemDetails(error: HttpErrorResponse): ProblemDetails {\r\n const body = error.error as Record<string, unknown> | null | undefined;\r\n const code = typeof body?.['code'] === 'string' ? (body['code'] as string) : `http.${error.status || 0}`;\r\n const message = typeof body?.['message'] === 'string' ? (body['message'] as string) : undefined;\r\n const required = typeof body?.['required'] === 'string' ? (body['required'] as string) : undefined;\r\n // Wire field is `correlation_id` (snake_case, ADR-017 §3) — kept as-is on the wire,\r\n // exposed as `correlationId` here per TS/Angular convention (same server-side pattern:\r\n // OrechioProblemDetails.CorrelationId is [JsonPropertyName(\"correlation_id\")]).\r\n const correlationId =\r\n typeof body?.['correlation_id'] === 'string' ? (body['correlation_id'] as string) : undefined;\r\n\r\n return {\r\n code,\r\n message,\r\n status: error.status,\r\n ...(required ? { required } : {}),\r\n ...(correlationId ? { correlationId } : {}),\r\n };\r\n}\r\n","/*\r\n * Public API Surface of @orechio/platform-angular\r\n */\r\n\r\nexport type { PlatformAngularConfig, PlatformAngularAuthPaths } from './lib/config/platform-angular-config';\r\nexport { PLATFORM_ANGULAR_CONFIG, providePlatformAngularConfig } from './lib/config/platform-angular-config';\r\n\r\nexport type { SessionUser } from './lib/session/session-user.model';\r\nexport { SessionStore } from './lib/session/session-store';\r\n\r\nexport { AuthClient } from './lib/auth/auth-client';\r\n\r\nexport { credentialsInterceptor, sessionExpiryInterceptor, providePlatformAngularHttp } from './lib/http/provide-platform-angular-http';\r\n\r\nexport type { CapabilityMap } from './lib/capabilities/capability.model';\r\nexport { hasCapability } from './lib/capabilities/capability.model';\r\nexport { CapabilityDirective } from './lib/capabilities/capability.directive';\r\n\r\nexport type { ProblemDetails } from './lib/problem-details/problem-details.model';\r\nexport { normalizeProblemDetails } from './lib/problem-details/problem-details.model';\r\n\r\nexport type { PagedResult } from './lib/pagination/paged-result.model';\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@orechio/platform-angular",
3
+ "version": "0.2.0",
4
+ "description": "Angular integration contracts for Orechio applications: BFF cookie-session auth client, session store, capabilities, runtime config, XSRF/HTTP interceptors, ProblemDetails and pagination.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/orechio-io/orechio-web.git"
9
+ },
10
+ "homepage": "https://github.com/orechio-io/orechio-web",
11
+ "bugs": {
12
+ "url": "https://github.com/orechio-io/orechio-web/issues"
13
+ },
14
+ "peerDependencies": {
15
+ "@angular/common": "^22.1.0",
16
+ "@angular/core": "^22.1.0",
17
+ "@angular/router": "^22.1.0",
18
+ "rxjs": "^7.8.0"
19
+ },
20
+ "dependencies": {
21
+ "tslib": "^2.3.0"
22
+ },
23
+ "sideEffects": false,
24
+ "module": "fesm2022/orechio-platform-angular.mjs",
25
+ "typings": "types/orechio-platform-angular.d.ts",
26
+ "exports": {
27
+ "./package.json": {
28
+ "default": "./package.json"
29
+ },
30
+ ".": {
31
+ "types": "./types/orechio-platform-angular.d.ts",
32
+ "default": "./fesm2022/orechio-platform-angular.mjs"
33
+ }
34
+ },
35
+ "type": "module"
36
+ }
@@ -0,0 +1,275 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, Provider, Signal } from '@angular/core';
3
+ import { Observable } from 'rxjs';
4
+ import { HttpInterceptorFn, HttpFeature, HttpFeatureKind, HttpErrorResponse } from '@angular/common/http';
5
+
6
+ /**
7
+ * BFF endpoint paths for the auth contract. All paths are relative to `apiBaseUrl`.
8
+ * `me` is optional: some BFFs bootstrap the session via `refresh` alone (no dedicated
9
+ * session-read endpoint) instead of a separate `me` call.
10
+ */
11
+ interface PlatformAngularAuthPaths {
12
+ login: string;
13
+ refresh: string;
14
+ logout: string;
15
+ me?: string;
16
+ /**
17
+ * Unwraps the raw auth-endpoint response body into TUser. Defaults to the
18
+ * identity function (response body IS the user object), matching Store's
19
+ * proven wire format. Portal's BFF wraps the user in an envelope
20
+ * (`{ user: UserInfo }`) — Portal supplies `(body) => body.user` here
21
+ * rather than the package assuming either shape.
22
+ */
23
+ unwrapUser?: (body: unknown) => unknown;
24
+ }
25
+ interface PlatformAngularConfig {
26
+ /** Same-origin API base path, e.g. '/api'. Never an absolute cross-origin URL — the
27
+ * BFF/cookie-session pattern this library assumes requires same-origin requests. */
28
+ apiBaseUrl: string;
29
+ auth: PlatformAngularAuthPaths;
30
+ /** XSRF cookie/header names. Defaults match Angular's own defaults and both proven
31
+ * consumers (Portal, Store): cookie 'XSRF-TOKEN', header 'X-XSRF-TOKEN'. */
32
+ xsrf?: {
33
+ cookieName?: string;
34
+ headerName?: string;
35
+ };
36
+ /** 401 handling strategy. 'redirect' clears the session and redirects immediately
37
+ * (Store's proven behavior). 'refresh-then-redirect' attempts one silent refresh
38
+ * before giving up (Portal's proven behavior). Defaults to 'redirect' — the simpler,
39
+ * more conservative behavior — when omitted. */
40
+ unauthorizedStrategy?: 'redirect' | 'refresh-then-redirect';
41
+ /** Route to navigate to after a session is cleared. Defaults to '/login'. */
42
+ loginRoute?: string;
43
+ }
44
+ declare const PLATFORM_ANGULAR_CONFIG: InjectionToken<PlatformAngularConfig>;
45
+ declare function providePlatformAngularConfig(config: PlatformAngularConfig): Provider;
46
+
47
+ /**
48
+ * Minimal common core of the session-user shape proven identical across both
49
+ * consumers (Portal's UserInfo, Store's Sessao). Product-specific fields
50
+ * (Portal's appId/branches[], Store's expiresAt/mustChangePassword) are NOT
51
+ * included here — extend this interface locally per product instead of
52
+ * requesting a package release for every product-specific field.
53
+ */
54
+ interface SessionUser {
55
+ userId: string | null;
56
+ tenantId: string | null;
57
+ branchId: string | null;
58
+ roles: string[];
59
+ name: string | null;
60
+ email: string | null;
61
+ }
62
+
63
+ /**
64
+ * Generic signal-based session store. Mirrors the pattern already proven
65
+ * independently by both consumers (Portal's AuthService.user, Store's
66
+ * EstadoDaSessao) — deliberately NOT a richer state machine (unknown /
67
+ * loading / error states): neither consumer needed one, and ADR-010 §2.2
68
+ * ("projete para extração, não extraia por antecipação") argues against
69
+ * building one speculatively.
70
+ *
71
+ * Parameterized over TUser so each product can extend SessionUser with its
72
+ * own fields without needing a package release.
73
+ */
74
+ declare class SessionStore<TUser extends SessionUser = SessionUser> {
75
+ private readonly state;
76
+ readonly user: Signal<TUser | null>;
77
+ readonly isAuthenticated: Signal<boolean>;
78
+ set(user: TUser): void;
79
+ clear(): void;
80
+ static ɵfac: i0.ɵɵFactoryDeclaration<SessionStore<any>, never>;
81
+ static ɵprov: i0.ɵɵInjectableDeclaration<SessionStore<any>>;
82
+ }
83
+
84
+ /**
85
+ * Thin BFF client for the auth contract: login / refresh / logout, and an
86
+ * optional session-read ("me") endpoint. Deliberately does NOT decode JWTs,
87
+ * store bearer tokens, or treat itself as a security authority — per
88
+ * ADR-010 §12 and this package's explicit non-goal (no JWT client SDK): the
89
+ * BFF/backend is the only place tokens live, this class only reflects
90
+ * whatever session-shaped JSON the BFF hands back.
91
+ *
92
+ * Endpoint paths are entirely config-driven (PLATFORM_ANGULAR_CONFIG) — this
93
+ * class contains zero hardcoded URLs, so it works unmodified against
94
+ * Portal's /api/auth/{login,me,refresh,logout} (envelope-wrapped
95
+ * `{ user: UserInfo }` responses, unwrapped via config.auth.unwrapUser) and
96
+ * Store's /api/auth/{login,refresh,logout} (no `me`, unwrapped responses)
97
+ * alike.
98
+ */
99
+ declare class AuthClient<TUser extends SessionUser = SessionUser> {
100
+ private readonly http;
101
+ private readonly config;
102
+ private readonly session;
103
+ private url;
104
+ private unwrap;
105
+ login(credentials: unknown): Observable<TUser>;
106
+ /**
107
+ * Reads the current session. Uses the configured `me` endpoint (GET) when
108
+ * present (Portal); falls back to `refresh` (POST) when it isn't (Store,
109
+ * which has no dedicated session-read endpoint and bootstraps via refresh).
110
+ */
111
+ initSession(): Observable<TUser>;
112
+ refresh(): Observable<TUser>;
113
+ /**
114
+ * Clears the local session unconditionally — on success, on error, and on
115
+ * unsubscribe (`finalize`, not `tap`) — matching the behavior both
116
+ * consumers already converged on independently (Portal swallows the
117
+ * error before clearing; Store clears via `finalize` while still letting
118
+ * the error propagate). This does NOT swallow errors: callers that want
119
+ * Portal's "always succeed" UX compose their own `catchError` on top.
120
+ */
121
+ logout(): Observable<void>;
122
+ static ɵfac: i0.ɵɵFactoryDeclaration<AuthClient<any>, never>;
123
+ static ɵprov: i0.ɵɵInjectableDeclaration<AuthClient<any>>;
124
+ }
125
+
126
+ /**
127
+ * Forces `withCredentials: true` on same-origin API requests, so the BFF's
128
+ * HttpOnly session cookie is sent. Proven identically in both consumers.
129
+ * XSRF header injection is intentionally NOT duplicated here — Angular's
130
+ * own `withXsrfConfiguration` (registered by `providePlatformAngularHttp`)
131
+ * already does that. Portal's local interceptor previously re-implemented
132
+ * XSRF header injection manually alongside `withXsrfConfiguration`; that
133
+ * was redundant, not a pattern to preserve.
134
+ */
135
+ declare const credentialsInterceptor: HttpInterceptorFn;
136
+ /**
137
+ * On 401 from a non-auth API request: 'redirect' (default) clears the
138
+ * session and navigates to the login route immediately (Store's proven
139
+ * behavior). 'refresh-then-redirect' attempts one silent refresh first and
140
+ * retries the original request on success, falling back to redirect
141
+ * (Portal's proven behavior). Configured via
142
+ * PlatformAngularConfig.unauthorizedStrategy.
143
+ *
144
+ * WEB-0.2: the 'refresh-then-redirect' path routes through `RefreshCoordinator`
145
+ * instead of calling `AuthClient.refresh()` directly — every 401 handled by
146
+ * the SAME running app shares one in-flight refresh instead of each request
147
+ * triggering its own. The 'redirect' strategy is untouched: it never calls
148
+ * refresh at all, so there is nothing to coordinate (see the config note on
149
+ * `PlatformAngularConfig.unauthorizedStrategy`). On refresh failure, the
150
+ * session-clear/navigate side effect now happens once inside the coordinator
151
+ * (shared across every waiting caller) rather than once per waiting caller;
152
+ * each caller still receives the ORIGINAL 401 error, not a refresh-specific
153
+ * one, exactly as before. Multi-tab/multi-window coordination is explicitly
154
+ * NOT handled here — see `RefreshCoordinator`'s doc comment — the Platform's
155
+ * own server-side retry leeway (ADR-016) is what makes concurrent refreshes
156
+ * from independent contexts safe.
157
+ */
158
+ declare const sessionExpiryInterceptor: HttpInterceptorFn;
159
+ /**
160
+ * Bundles provideHttpClient with the XSRF configuration and interceptors
161
+ * proven necessary by both consumers. Cookie/header names default to
162
+ * 'XSRF-TOKEN'/'X-XSRF-TOKEN' (identical in both consumers today) but
163
+ * remain overridable via config. Consumers add their own extra
164
+ * provideHttpClient features (e.g. Portal's withXhr()) by calling
165
+ * provideHttpClient(...) separately — this function only owns the concerns
166
+ * proven common to both.
167
+ */
168
+ declare function providePlatformAngularHttp(...extraFeatures: HttpFeature<HttpFeatureKind>[]): i0.EnvironmentProviders;
169
+
170
+ /**
171
+ * Generic capability map: a flat set of boolean feature flags fetched once
172
+ * from the BFF (Portal's proven pattern — GET .../capabilities returning a
173
+ * flat boolean-keyed object). Deliberately a generic `Record<string,
174
+ * boolean>` rather than a typed interface enumerating every field: Portal's
175
+ * capability set already has 17 product-specific fields and Store will
176
+ * grow its own, unrelated set. A typed interface would require a package
177
+ * release for every new product capability (see ADR-010 §18 — evolution
178
+ * cadence mismatch is a real argument against sharing). Each product
179
+ * defines its own capability keys locally and reads them through this map.
180
+ *
181
+ * UX ONLY. This does not replace server-side authorization. Every consumer
182
+ * MUST continue to handle 403 from the actual mutating/reading endpoint —
183
+ * capability checks here are a consultive pre-check, exactly as already
184
+ * documented in Portal's application.service.ts before this extraction.
185
+ */
186
+ type CapabilityMap = Readonly<Record<string, boolean>>;
187
+ declare function hasCapability(capabilities: CapabilityMap | null | undefined, key: string): boolean;
188
+
189
+ /**
190
+ * Structural directive gating template content on a capability key from a
191
+ * consumer-supplied CapabilityMap. Net-new (neither consumer had factored
192
+ * their proven inline `@if (capabilities()?.x)` pattern into a directive),
193
+ * but grounded in Portal's validated usage — see capability.model.ts.
194
+ *
195
+ * UX ONLY — see capability.model.ts. Hiding an element here does not
196
+ * authorize anything; the backend re-validates on every mutating call.
197
+ *
198
+ * Usage: `<button *orxCapability="capabilities(); has: 'manageTenants'">`
199
+ */
200
+ declare class CapabilityDirective {
201
+ private readonly templateRef;
202
+ private readonly viewContainer;
203
+ private map;
204
+ private key;
205
+ private hasView;
206
+ set orxCapability(map: CapabilityMap | null | undefined);
207
+ set has(key: string);
208
+ private render;
209
+ static ɵfac: i0.ɵɵFactoryDeclaration<CapabilityDirective, never>;
210
+ static ɵdir: i0.ɵɵDirectiveDeclaration<CapabilityDirective, "[orxCapability]", never, { "orxCapability": { "alias": "orxCapability"; "required": false; }; "has": { "alias": "orxCapabilityHas"; "required": false; }; }, {}, never, never, true, never>;
211
+ }
212
+
213
+ /**
214
+ * Canonical error shape, generalized from Store's already-proven
215
+ * ErroApresentavel/normalizarErroApi (the more mature of the two consumers'
216
+ * error handling — Portal had no typed contract at all, just five
217
+ * duplicated `error.error.code` reads). `message` is optional: Portal never
218
+ * received a server-supplied message field, only `code`, and resolves
219
+ * user-facing text locally — so this contract cannot require it.
220
+ *
221
+ * `correlation_id` (API-1, ADR-017 §3.3): added after confirming directly
222
+ * from Orechio.Platform's `ProblemResponses.cs` that it IS present in the
223
+ * JSON body of every error response the Platform host produces (not just
224
+ * the `X-Correlation-ID` response header), and that Portal's BFF relays
225
+ * that body byte-for-byte, unreshaped, for every typed endpoint. The
226
+ * earlier NG-1-era absence was accurate for what it tested at the time but
227
+ * incomplete relative to Platform's actual wire contract. Optional: not
228
+ * every error path in every consumer emits this field today (Store's own
229
+ * narrower auth/CSRF error shapes don't) — its absence must not be treated
230
+ * as a parse failure.
231
+ *
232
+ * Domain-specific error codes (e.g. 'access.platform_only',
233
+ * 'tenant.not_found', 'csrf.invalid') are NOT enumerated here — `code`
234
+ * stays a plain string so any code survives normalization unmodified and
235
+ * remains identifiable by callers, per Passo 28.
236
+ */
237
+ interface ProblemDetails {
238
+ code: string;
239
+ message?: string;
240
+ status: number;
241
+ required?: string;
242
+ correlationId?: string;
243
+ }
244
+ /**
245
+ * Normalizes an HttpErrorResponse into ProblemDetails. Mirrors Store's
246
+ * normalizarErroApi exactly, generalized to make `message` optional and to
247
+ * accept an HttpErrorResponse without any consumer-specific fallback
248
+ * message map (each product keeps its own code→message copy locally —
249
+ * that's product-specific text, not shared infrastructure).
250
+ */
251
+ declare function normalizeProblemDetails(error: HttpErrorResponse): ProblemDetails;
252
+
253
+ /**
254
+ * Canonical paged-result envelope, mirroring
255
+ * Orechio.BuildingBlocks.Pagination.PageResult<T> (ADR-010 §7 / doc 011 §3)
256
+ * on the wire — Portal's InvoicePage already matches this shape field for
257
+ * field (it was generic'd from a concrete, non-generic type here). Per
258
+ * ADR-010 §24.4, the ecosystem must not grow parallel paging conventions;
259
+ * Store's local `PaginaResultado<T>` (Portuguese field names) is exactly
260
+ * the kind of parallel convention this canonical shape replaces — Store's
261
+ * migration to this type is deferred to a future phase pending
262
+ * confirmation of its actual wire format (see NG-1 report, item 56).
263
+ */
264
+ interface PagedResult<T> {
265
+ items: T[];
266
+ page: number;
267
+ pageSize: number;
268
+ totalItems: number;
269
+ totalPages: number;
270
+ hasNext: boolean;
271
+ hasPrevious: boolean;
272
+ }
273
+
274
+ export { AuthClient, CapabilityDirective, PLATFORM_ANGULAR_CONFIG, SessionStore, credentialsInterceptor, hasCapability, normalizeProblemDetails, providePlatformAngularConfig, providePlatformAngularHttp, sessionExpiryInterceptor };
275
+ export type { CapabilityMap, PagedResult, PlatformAngularAuthPaths, PlatformAngularConfig, ProblemDetails, SessionUser };