ptech-shell-dev 0.1.0 → 1.6.3

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 CHANGED
@@ -14,4 +14,23 @@ npm i ptech-shell-dev ptech-shell-sdk
14
14
  import { initStandaloneServices } from 'ptech-shell-dev';
15
15
 
16
16
  initStandaloneServices();
17
- ```
17
+ ```
18
+
19
+ ## MSAL adapter
20
+
21
+ ```ts
22
+ import { registerService, TOKENS } from 'ptech-shell-sdk';
23
+ import { PublicClientApplication } from '@azure/msal-browser';
24
+ import { createMsalUserService } from 'ptech-shell-dev';
25
+
26
+ const msal = new PublicClientApplication(msalConfig);
27
+
28
+ registerService(
29
+ TOKENS.userService,
30
+ createMsalUserService({
31
+ msal,
32
+ defaultScopes: ['User.Read'],
33
+ loginMode: 'popup',
34
+ }),
35
+ );
36
+ ```
package/dist/index.d.ts CHANGED
@@ -1,4 +1,65 @@
1
- import { Lang, User, I18nService, UserService, ApiClient, NavigationService, ConfigService, PermissionService, SharedStateService, ObservabilityService, NotificationService, AnalyticsService } from 'ptech-shell-sdk';
1
+ import { LogLevel, RealtimeEnvelope, UserService, RequestContextService, RealtimeService, Lang, User, I18nService, ApiClient, NavigationService, ConfigService, PermissionService, SharedStateService, ObservabilityService, NotificationService, AnalyticsService, ObservabilityConfigPatch, ApiErrorPayload, ApiValidationErrors, ApiRetryOptions, MsalAccountInfoLike, NavigationAction, NavigationTo, NavigationNavigateOptions } from 'ptech-shell-sdk';
2
+
3
+ type ObservabilityTelemetryEvent = {
4
+ level: LogLevel;
5
+ source: string;
6
+ message: string;
7
+ timestamp: number;
8
+ traceId?: string;
9
+ correlationId?: string;
10
+ data?: unknown;
11
+ };
12
+ type ObservabilityTelemetrySink = {
13
+ track: (event: ObservabilityTelemetryEvent) => void | Promise<void>;
14
+ };
15
+ type CreateStandaloneObservabilityServiceOptions = {
16
+ telemetrySink?: ObservabilityTelemetrySink;
17
+ redact?: (data: unknown) => unknown;
18
+ getTraceContext?: () => {
19
+ traceId?: string;
20
+ correlationId?: string;
21
+ };
22
+ };
23
+
24
+ type RealtimeEventListener = (envelope: RealtimeEnvelope<unknown>) => void;
25
+ type RealtimeReconnectOptions = {
26
+ initialDelayMs?: number;
27
+ maxDelayMs?: number;
28
+ maxAttempts?: number;
29
+ };
30
+ type RealtimeTransportConnectContext = {
31
+ accessToken?: string;
32
+ traceId?: string;
33
+ correlationId?: string;
34
+ };
35
+ type RealtimeTransport = {
36
+ start: (context: RealtimeTransportConnectContext) => Promise<void>;
37
+ stop: () => Promise<void>;
38
+ onMessage: (listener: RealtimeEventListener) => () => void;
39
+ onDisconnect?: (listener: () => void) => () => void;
40
+ send?: (event: string, payload: unknown) => Promise<void>;
41
+ };
42
+ type CreateStandaloneRealtimeServiceOptions = {
43
+ createTransport?: () => RealtimeTransport;
44
+ reconnect?: RealtimeReconnectOptions;
45
+ getUserService?: () => UserService | undefined;
46
+ userService?: UserService;
47
+ getRequestContext?: () => RequestContextService | undefined;
48
+ requestContext?: RequestContextService;
49
+ };
50
+ /**
51
+ * WHY:
52
+ * - Creates a standalone realtime service with reconnect, listener fan-out, and trace propagation.
53
+ * WHEN TO USE:
54
+ * - Use in shell/host apps that need lightweight realtime orchestration independent from framework lifecycle.
55
+ * WHEN NOT TO USE:
56
+ * - Do not call repeatedly for the same logical channel unless isolation is required.
57
+ * INVARIANTS:
58
+ * - Connection state transitions are serialized through internal guards.
59
+ * - `start()` is idempotent while already connected/connecting/reconnecting.
60
+ * - `stop()` always leaves state at `disconnected` and clears timers/listeners.
61
+ */
62
+ declare function createStandaloneRealtimeService(options?: CreateStandaloneRealtimeServiceOptions): RealtimeService;
2
63
 
3
64
  type StandaloneServices = {
4
65
  i18n: I18nService;
@@ -8,6 +69,8 @@ type StandaloneServices = {
8
69
  configService: ConfigService;
9
70
  permissionService: PermissionService;
10
71
  sharedState: SharedStateService;
72
+ requestContext: RequestContextService;
73
+ realtime: RealtimeService;
11
74
  observability: ObservabilityService;
12
75
  notification: NotificationService;
13
76
  analytics: AnalyticsService;
@@ -32,6 +95,21 @@ type StandaloneInitOptions = {
32
95
  * This is useful when each remote wants slightly different mock behavior.
33
96
  */
34
97
  customize?: (services: StandaloneServices) => void;
98
+ /**
99
+ * Optional observability log filter.
100
+ * Example:
101
+ * - dev: { minLevel: 'debug' }
102
+ * - production: { minLevel: 'error' }
103
+ */
104
+ observability?: ObservabilityConfigPatch;
105
+ /**
106
+ * Optional observability adapters (for App Insights/OpenTelemetry bridges).
107
+ */
108
+ observabilityOptions?: Omit<CreateStandaloneObservabilityServiceOptions, 'getTraceContext'>;
109
+ /**
110
+ * Realtime bootstrap options.
111
+ */
112
+ realtime?: Omit<CreateStandaloneRealtimeServiceOptions, 'getUserService' | 'userService' | 'getRequestContext' | 'requestContext'>;
35
113
  /**
36
114
  * Service registration strategy for standalone init.
37
115
  * - if-missing (default): only register when token has no service yet.
@@ -48,10 +126,254 @@ type StandaloneInitOptions = {
48
126
  */
49
127
  declare function initStandaloneServices(options?: StandaloneInitOptions): void;
50
128
 
51
- declare function createStandaloneApiClient(apiBase: string): ApiClient;
129
+ type CreateStandaloneApiClientOptions = {
130
+ apiBase: string;
131
+ userService?: UserService;
132
+ notificationService?: NotificationService;
133
+ /**
134
+ * Optional dynamic lookup. Useful if the user service can be replaced after initialization.
135
+ */
136
+ getUserService?: () => UserService | undefined;
137
+ /**
138
+ * Optional dynamic lookup. Useful if the notification service can be replaced after initialization.
139
+ */
140
+ getNotificationService?: () => NotificationService | undefined;
141
+ /**
142
+ * Optional dynamic lookup. Useful if request context service can be replaced after initialization.
143
+ */
144
+ getRequestContext?: () => RequestContextService | undefined;
145
+ requestContext?: RequestContextService;
146
+ defaultAuthScopes?: string[];
147
+ defaultTimeoutMs?: number;
148
+ defaultRetries?: ApiRetryOptions | false;
149
+ };
150
+ /**
151
+ * WHY:
152
+ * - Standardized API error object carrying HTTP/network/auth/trace context for UI and logging.
153
+ * WHEN TO USE:
154
+ * - Throw from API client internals whenever request/response contract fails.
155
+ * WHEN NOT TO USE:
156
+ * - Do not throw raw unknown errors across service boundary when this shape is expected.
157
+ * INVARIANTS:
158
+ * - Flags (`isNetworkError`, `isAuthError`, `isTimeoutError`) are always explicitly set.
159
+ * - Optional diagnostics (`traceId`, `correlationId`, `validationErrors`) are preserved when available.
160
+ */
161
+ declare class ApiError extends Error implements ApiErrorPayload {
162
+ readonly status?: number;
163
+ readonly code?: string;
164
+ readonly traceId?: string;
165
+ readonly correlationId?: string;
166
+ readonly isNetworkError: boolean;
167
+ readonly isAuthError: boolean;
168
+ readonly isTimeoutError: boolean;
169
+ readonly validationErrors?: ApiValidationErrors;
170
+ readonly cause?: unknown;
171
+ constructor(details: ApiErrorPayload, cause?: unknown);
172
+ }
173
+ /**
174
+ * WHY:
175
+ * - Narrowing utility for callers handling mixed thrown values.
176
+ * WHEN TO USE:
177
+ * - Use in catch blocks to branch on structured `ApiError`.
178
+ * WHEN NOT TO USE:
179
+ * - Do not use for cross-realm error checks where `instanceof` may fail.
180
+ * INVARIANTS:
181
+ * - Returns true only for instances created from this `ApiError` class.
182
+ */
183
+ declare function isApiError(value: unknown): value is ApiError;
184
+ /**
185
+ * WHY:
186
+ * - Factory for standalone API client with shared auth, retry, tracing, and error normalization policies.
187
+ * WHEN TO USE:
188
+ * - Use as the single HTTP entrypoint for shell modules to keep behavior consistent.
189
+ * WHEN NOT TO USE:
190
+ * - Do not bypass with direct `fetch` for domain calls that require standard error/context handling.
191
+ * INVARIANTS:
192
+ * - All request methods delegate to common execution pipeline.
193
+ * - Returned client honors configured defaults unless request overrides them.
194
+ */
195
+ declare function createStandaloneApiClient(options: string | CreateStandaloneApiClientOptions): ApiClient;
52
196
 
197
+ type UiApiError = {
198
+ i18nKey: string;
199
+ params: Record<string, string | number>;
200
+ supportTraceId?: string;
201
+ severity: 'error' | 'warning';
202
+ };
203
+ /**
204
+ * WHY:
205
+ * - Converts transport/domain API errors into a stable UI error contract based on i18n keys.
206
+ * WHEN TO USE:
207
+ * - Use at UI boundary (toasts, banners, inline errors) after API layer has produced `ApiErrorPayload`.
208
+ * WHEN NOT TO USE:
209
+ * - Do not bypass this mapper with raw server messages in user-facing UI.
210
+ * INVARIANTS:
211
+ * - Always returns a defined `i18nKey`, `params`, and `severity`.
212
+ * - Prefer traceability by forwarding `traceId` or `correlationId` when available.
213
+ * - Auth/network/timeout/validation precedence is deterministic.
214
+ */
215
+ declare function mapApiErrorToUiError(error: ApiErrorPayload): UiApiError;
216
+
217
+ /**
218
+ * WHY:
219
+ * - Implements a simple namespaced i18n store for standalone shell environments.
220
+ * WHEN TO USE:
221
+ * - Use when features need runtime language switching and message registration in dev shell.
222
+ * WHEN NOT TO USE:
223
+ * - Do not use as a full ICU/message-format engine.
224
+ * INVARIANTS:
225
+ * - Translation lookup is deterministic by namespace insertion order.
226
+ * - Missing key falls back to the key itself.
227
+ * - Base namespace (`shell-dev`) cannot be unregistered.
228
+ */
53
229
  declare function createStandaloneI18nService(initialLang: Lang): I18nService;
54
230
 
55
- declare function createStandaloneUserService(user: User | null): UserService;
231
+ type Claims = Record<string, unknown>;
232
+ type MsalEventMessageLike = {
233
+ eventType?: string;
234
+ payload?: unknown;
235
+ error?: unknown;
236
+ };
237
+ type MsalLoginRequestLike = {
238
+ scopes?: string[];
239
+ loginHint?: string;
240
+ prompt?: string;
241
+ claims?: string;
242
+ };
243
+ type MsalSilentTokenRequestLike = {
244
+ scopes: string[];
245
+ account?: MsalAccountInfoLike;
246
+ forceRefresh?: boolean;
247
+ claims?: string;
248
+ };
249
+ type MsalPopupTokenRequestLike = {
250
+ scopes: string[];
251
+ account?: MsalAccountInfoLike;
252
+ claims?: string;
253
+ loginHint?: string;
254
+ };
255
+ type MsalTokenResponseLike = {
256
+ accessToken: string;
257
+ tokenType?: string;
258
+ scopes?: string[];
259
+ expiresOn?: Date | null;
260
+ idToken?: string;
261
+ idTokenClaims?: Claims;
262
+ account?: MsalAccountInfoLike | null;
263
+ };
264
+ type MsalLoginResponseLike = {
265
+ account?: MsalAccountInfoLike | null;
266
+ idToken?: string;
267
+ idTokenClaims?: Claims;
268
+ };
269
+ type MsalLogoutRequestLike = {
270
+ account?: MsalAccountInfoLike;
271
+ postLogoutRedirectUri?: string;
272
+ };
273
+ type MsalBrowserInstanceLike = {
274
+ getActiveAccount: () => MsalAccountInfoLike | null;
275
+ setActiveAccount?: (account: MsalAccountInfoLike | null) => void;
276
+ getAllAccounts: () => MsalAccountInfoLike[];
277
+ addEventCallback?: (callback: (message: MsalEventMessageLike) => void) => string | null;
278
+ removeEventCallback?: (callbackId: string) => void;
279
+ loginPopup?: (request?: MsalLoginRequestLike) => Promise<MsalLoginResponseLike>;
280
+ loginRedirect?: (request?: MsalLoginRequestLike) => Promise<void>;
281
+ logoutPopup?: (request?: MsalLogoutRequestLike) => Promise<void>;
282
+ logoutRedirect?: (request?: MsalLogoutRequestLike) => Promise<void>;
283
+ acquireTokenSilent: (request: MsalSilentTokenRequestLike) => Promise<MsalTokenResponseLike>;
284
+ acquireTokenPopup?: (request: MsalPopupTokenRequestLike) => Promise<MsalTokenResponseLike>;
285
+ };
286
+ type MsalInteractionMode = 'popup' | 'redirect';
287
+ type CreateMsalUserServiceOptions = {
288
+ msal: MsalBrowserInstanceLike;
289
+ defaultScopes?: string[];
290
+ loginMode?: MsalInteractionMode;
291
+ logoutMode?: MsalInteractionMode;
292
+ mapUser?: (account: MsalAccountInfoLike) => User;
293
+ resolveAccount?: (accounts: MsalAccountInfoLike[]) => MsalAccountInfoLike | null;
294
+ };
295
+ /**
296
+ * WHY:
297
+ * - Implements shell `UserService` on top of MSAL browser APIs.
298
+ * WHEN TO USE:
299
+ * - Use in host shells that authenticate with Microsoft Entra/MSAL.
300
+ * WHEN NOT TO USE:
301
+ * - Do not use without valid MSAL instance contract implementation.
302
+ * INVARIANTS:
303
+ * - Access token acquisition prefers silent flow and falls back to popup when available.
304
+ * - Session snapshot always derives from current active/resolved account.
305
+ * - Event callback is attached only while there is at least one subscriber.
306
+ */
307
+ declare function createMsalUserService(options: CreateMsalUserServiceOptions): UserService;
308
+
309
+ type ReactRouterLocationLike = {
310
+ pathname: string;
311
+ search?: string;
312
+ hash?: string;
313
+ state?: unknown;
314
+ key?: string;
315
+ };
316
+ type ReactRouterNavigateFunctionLike = (to: NavigationTo | number, options?: Omit<NavigationNavigateOptions, 'source'>) => void;
317
+ type ReactRouterCreateHrefFunctionLike = (to: NavigationTo) => string;
318
+ type ReactRouterNavigationCapabilities = {
319
+ canGoBack?: boolean;
320
+ canGoForward?: boolean;
321
+ historyIndex?: number;
322
+ };
323
+ type CreateReactRouterNavigationAdapterOptions = {
324
+ initialLocation?: ReactRouterLocationLike;
325
+ initialAction?: NavigationAction;
326
+ };
327
+ type ReactRouterNavigationAdapter = {
328
+ service: NavigationService;
329
+ bindNavigator: (navigate: ReactRouterNavigateFunctionLike, createHref?: ReactRouterCreateHrefFunctionLike) => void;
330
+ clearNavigator: () => void;
331
+ syncFromRouter: (location: ReactRouterLocationLike, action: NavigationAction, capabilities?: ReactRouterNavigationCapabilities) => void;
332
+ };
333
+ /**
334
+ * WHY:
335
+ * - Bridges React Router navigation primitives into the shell `NavigationService` contract.
336
+ * WHEN TO USE:
337
+ * - Use when host app runs React Router and remote modules consume shell navigation service.
338
+ * WHEN NOT TO USE:
339
+ * - Do not use when navigation source is not React Router-like.
340
+ * INVARIANTS:
341
+ * - Adapter never throws when navigator functions are temporarily unavailable.
342
+ * - Snapshot updates emit only on meaningful state/location changes.
343
+ * - Location fields remain normalized for cross-module consistency.
344
+ */
345
+ declare function createReactRouterNavigationAdapter(options?: CreateReactRouterNavigationAdapterOptions): ReactRouterNavigationAdapter;
346
+
347
+ type CreateRequestContextServiceOptions = {
348
+ traceId?: string;
349
+ correlationId?: string;
350
+ };
351
+ /**
352
+ * WHY:
353
+ * - Holds request-scoped trace/correlation IDs and notifies consumers on mutation.
354
+ * WHEN TO USE:
355
+ * - Use as shared context service for API/realtime observability correlation.
356
+ * WHEN NOT TO USE:
357
+ * - Do not use as distributed trace store across processes.
358
+ * INVARIANTS:
359
+ * - `getSnapshot` returns a copy, never the mutable internal reference.
360
+ * - `update` emits only when values actually change.
361
+ * - `rotate` always regenerates both IDs.
362
+ */
363
+ declare function createRequestContextService(options?: CreateRequestContextServiceOptions): RequestContextService;
364
+
365
+ /**
366
+ * WHY:
367
+ * - Provides mock user/session/auth token behavior for standalone shell development.
368
+ * WHEN TO USE:
369
+ * - Use when integrating features against `UserService` without real identity provider.
370
+ * WHEN NOT TO USE:
371
+ * - Do not use as secure authentication provider in production.
372
+ * INVARIANTS:
373
+ * - Session state is derived directly from `currentUser` presence.
374
+ * - `login` is no-op when already authenticated.
375
+ * - `logout` clears user state and notifies subscribers.
376
+ */
377
+ declare function createStandaloneUserService(seedUser: User | null): UserService;
56
378
 
57
- export { type ServiceRegisterMode, type StandaloneInitOptions, type StandaloneServices, createStandaloneApiClient, createStandaloneI18nService, createStandaloneUserService, initStandaloneServices };
379
+ export { ApiError, type CreateMsalUserServiceOptions, type CreateReactRouterNavigationAdapterOptions, type CreateStandaloneApiClientOptions, type CreateStandaloneObservabilityServiceOptions, type CreateStandaloneRealtimeServiceOptions, type MsalBrowserInstanceLike, type ObservabilityTelemetryEvent, type ObservabilityTelemetrySink, type ReactRouterCreateHrefFunctionLike, type ReactRouterLocationLike, type ReactRouterNavigateFunctionLike, type ReactRouterNavigationAdapter, type ReactRouterNavigationCapabilities, type RealtimeReconnectOptions, type RealtimeTransport, type RealtimeTransportConnectContext, type ServiceRegisterMode, type StandaloneInitOptions, type StandaloneServices, type UiApiError, createMsalUserService, createReactRouterNavigationAdapter, createRequestContextService, createStandaloneApiClient, createStandaloneI18nService, createStandaloneRealtimeService, createStandaloneUserService, initStandaloneServices, isApiError, mapApiErrorToUiError };