@powerportalspro/core 5.0.0-beta.1

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/dist/index.cjs ADDED
@@ -0,0 +1,1263 @@
1
+ 'use strict';
2
+
3
+ // src/routes.ts
4
+ var apiRoot = "/api";
5
+ var authRoot = `${apiRoot}/auth`;
6
+ var manageRoot = `${authRoot}/manage`;
7
+ var tables = {
8
+ /** Template: `/api/table/{tableLogicalName}`. POST creates a record. */
9
+ createRoute: `${apiRoot}/table/{tableLogicalName}`,
10
+ /** Template: `/api/table/{tableLogicalName}/{recordId}`. GET reads, PATCH updates, DELETE removes. */
11
+ readUpdateDeleteRoute: `${apiRoot}/table/{tableLogicalName}/{recordId}`,
12
+ /** Resolved URL for creating a record of the given table. */
13
+ getCreateRoute: (tableLogicalName) => `${apiRoot}/table/${tableLogicalName}`,
14
+ /**
15
+ * Resolved URL for retrieving a specific record, optionally with a column allow-list.
16
+ * @param columns Optional list of column logical names — joined with `,` and sent as `?columns=`.
17
+ */
18
+ getRetrieveRoute: (tableLogicalName, recordId, columns) => {
19
+ const base = `${apiRoot}/table/${tableLogicalName}/${recordId}`;
20
+ return columns && columns.length > 0 ? `${base}?columns=${columns.join(",")}` : base;
21
+ },
22
+ /** Resolved URL for updating a record (PATCH). */
23
+ getUpdateRoute: (tableLogicalName, recordId) => `${apiRoot}/table/${tableLogicalName}/${recordId}`,
24
+ /** Resolved URL for deleting a record (DELETE). */
25
+ getDeleteRoute: (tableLogicalName, recordId) => `${apiRoot}/table/${tableLogicalName}/${recordId}`
26
+ };
27
+ var manage = {
28
+ /** Base path under which all manage endpoints live (`/api/auth/manage`). */
29
+ root: manageRoot,
30
+ /** GET — read first/last/mobile from the linked Dataverse contact + Identity status flags. */
31
+ profile: `${manageRoot}/profile`,
32
+ /** POST — update first/last/mobile on the linked Dataverse contact. */
33
+ updateProfile: `${manageRoot}/profile`,
34
+ /** POST — add a local password to an account that doesn't have one. */
35
+ setPassword: `${manageRoot}/password/set`,
36
+ /** POST — change the local password (requires the current password). */
37
+ changePassword: `${manageRoot}/password/change`,
38
+ /** POST — start a change-email flow by emailing the new address a confirmation link. */
39
+ changeEmail: `${manageRoot}/email/change`,
40
+ /** POST — re-send the confirmation link to the user's current email. */
41
+ sendEmailConfirmation: `${manageRoot}/email/send-confirmation`,
42
+ /** GET — read 2FA status. */
43
+ twoFactorStatus: `${manageRoot}/2fa`,
44
+ /** GET — shared key + otpauth URI for the QR-code enrollment screen. */
45
+ authenticatorSetup: `${manageRoot}/authenticator/setup`,
46
+ /** POST — verify a code from the authenticator app. */
47
+ verifyAuthenticator: `${manageRoot}/authenticator/verify`,
48
+ /** POST — rotate the authenticator key (also disables 2FA). */
49
+ resetAuthenticator: `${manageRoot}/authenticator/reset`,
50
+ /** POST — disable 2FA on the account. */
51
+ disable2fa: `${manageRoot}/2fa/disable`,
52
+ /** POST — regenerate the user's recovery codes. */
53
+ generateRecoveryCodes: `${manageRoot}/2fa/recovery-codes/generate`,
54
+ /** POST — clear this browser's "trust this device" 2FA cookie. */
55
+ forget2faClient: `${manageRoot}/2fa/forget-browser`,
56
+ /** GET — list the signed-in user's linked external logins + available providers to add. */
57
+ externalLogins: `${manageRoot}/external-logins`,
58
+ /** GET — combined snapshot of sign-in paths (linked external logins + whether a local password exists). */
59
+ loginInfo: `${manageRoot}/login-info`,
60
+ /** GET — kicks off the OAuth flow that links an additional external login. Full-page navigate, do not fetch. */
61
+ linkExternalLogin: `${manageRoot}/external-logins/link`,
62
+ /** GET — OAuth callback target for {@link linkExternalLogin}. */
63
+ linkExternalLoginCallback: `${manageRoot}/external-logins/link/callback`,
64
+ /** POST — remove one external login by provider + key. */
65
+ removeExternalLogin: `${manageRoot}/external-logins/remove`,
66
+ /** GET — dump every `[PersonalData]` property + linked external logins. */
67
+ personalData: `${manageRoot}/personal-data`,
68
+ /** POST — permanently delete the user's account. */
69
+ deletePersonalData: `${manageRoot}/personal-data/delete`,
70
+ /** Resolved URL for {@link linkExternalLogin} with the provider + return URL pre-encoded. */
71
+ getLinkExternalLoginRoute: (provider, returnUrl) => {
72
+ const query = `?provider=${encodeURIComponent(provider)}`;
73
+ return returnUrl ? `${manageRoot}/external-logins/link${query}&returnUrl=${encodeURIComponent(returnUrl)}` : `${manageRoot}/external-logins/link${query}`;
74
+ }
75
+ };
76
+ var auth = {
77
+ /** Base path under which all auth endpoints live (`/api/auth`). */
78
+ root: authRoot,
79
+ /** POST — submit credentials. */
80
+ login: `${authRoot}/login`,
81
+ /** POST — submit the second-factor code after a Login that returned RequiresTwoFactor. */
82
+ loginTwoFactor: `${authRoot}/login/2fa`,
83
+ /** POST — clear the auth cookie. */
84
+ logout: `${authRoot}/logout`,
85
+ /** POST — create a new user account. */
86
+ register: `${authRoot}/register`,
87
+ /** POST — send a password-reset email. */
88
+ forgotPassword: `${authRoot}/forgot-password`,
89
+ /** POST — complete a password reset with the email-link token. */
90
+ resetPassword: `${authRoot}/reset-password`,
91
+ /** POST — confirm a registered user's email with the email-link token. */
92
+ confirmEmail: `${authRoot}/confirm-email`,
93
+ /** POST — send a fresh confirmation email. */
94
+ resendEmailConfirmation: `${authRoot}/resend-email-confirmation`,
95
+ /** GET — list of configured external (OAuth) login providers. */
96
+ externalProviders: `${authRoot}/external-providers`,
97
+ /** GET — kicks off the OAuth flow for the named external provider. Full-page navigate, do not fetch. */
98
+ externalLogin: `${authRoot}/external-login`,
99
+ /** GET — snapshot the in-flight external-login cookie set by the OAuth callback. */
100
+ externalLoginPending: `${authRoot}/external-login/pending`,
101
+ /** POST — complete an in-flight external login by registering / linking the account. */
102
+ externalLoginConfirm: `${authRoot}/external-login/confirm`,
103
+ /** POST — complete an in-flight external login when the user has chosen between multiple matching identities. */
104
+ externalLoginSelect: `${authRoot}/external-login/select`,
105
+ /** GET — write the chooser's "remember my choice" cookie + redirect to `returnUrl`. */
106
+ externalLoginRememberChoice: `${authRoot}/external-login/remember-choice`,
107
+ /** POST — swap the current cookie for the user's alt identity (the contact↔systemuser sibling). */
108
+ switchIdentity: `${authRoot}/switch-identity`,
109
+ /** GET — snapshot of the current user (or anonymous shape). */
110
+ me: `${authRoot}/me`,
111
+ /** Account-management endpoints under `/api/auth/manage/*`. */
112
+ manage,
113
+ /** Resolved URL for {@link externalLogin} with the provider + return URL pre-encoded. */
114
+ getExternalLoginRoute: (provider, returnUrl) => {
115
+ const query = `?provider=${encodeURIComponent(provider)}`;
116
+ return returnUrl ? `${authRoot}/external-login${query}&returnUrl=${encodeURIComponent(returnUrl)}` : `${authRoot}/external-login${query}`;
117
+ }
118
+ };
119
+ var api = {
120
+ /** Base path for the API (`/api`). */
121
+ root: apiRoot,
122
+ /** Template — POST a FetchXML query to retrieve multiple records. */
123
+ retrieveMultiple: `${apiRoot}/retrieveMultiple`,
124
+ /** Template — GET table metadata. */
125
+ retrieveTableMetadata: `${apiRoot}/tableMetadata/{tableLogicalName}`,
126
+ /** Template — GET view metadata by view id (Guid-constrained route). */
127
+ retrieveViewMetadata: `${apiRoot}/viewMetadata/{viewId}`,
128
+ /** Template — GET every view metadata record for a table. */
129
+ retrieveViewsForTable: `${apiRoot}/viewMetadata/{tableLogicalName}`,
130
+ /** Template — GET localized strings for a culture. */
131
+ retrieveLocalizedStrings: `${apiRoot}/localizedStrings/{culture}`,
132
+ /** Template — POST a batch of OrganizationRequest payloads. */
133
+ executeMultiple: `${apiRoot}/executeMultiple`,
134
+ /** GET — environment-wide file upload settings (blocked extensions, max upload size). */
135
+ environmentFileSettings: `${apiRoot}/environmentFileSettings`,
136
+ /** POST — clear all server-side caches. SystemAdmin-only. */
137
+ clearAllCaches: `${apiRoot}/caches/clear`,
138
+ /** GET — every server-side cache name. SystemAdmin-only. */
139
+ cacheNames: `${apiRoot}/caches`,
140
+ /** Template — POST to clear a single named cache. SystemAdmin-only. */
141
+ clearCache: `${apiRoot}/caches/{cacheName}/clear`,
142
+ /** Template — GET file metadata + (optionally) content for one record/column. */
143
+ retrieveFileInfo: `${apiRoot}/files/{tableLogicalName}/{recordId}/{columnName}`,
144
+ /** Template — POST record ids; returns file info + content for many records of one column. */
145
+ retrieveFilesBatch: `${apiRoot}/files/{tableLogicalName}/{columnName}/batch`,
146
+ /**
147
+ * POST — builds an archive containing the file payload of every supplied
148
+ * record id for the named table/column. Server returns the binary archive
149
+ * bytes (default) or a JSON envelope when `responseFormat` is `Json`.
150
+ */
151
+ createFileArchive: `${apiRoot}/files/createFileArchive`,
152
+ /** Resolved URL for retrieving multiple records via FetchXML. */
153
+ getRetrieveMultipleRoute: (fetchXml) => `${apiRoot}/retrieveMultiple?fetchXml=${encodeURIComponent(fetchXml)}`,
154
+ /**
155
+ * Template — POST a GridDataRequest (viewId / fetchXml input modes,
156
+ * searchText, sorts, paging, filters) and get back a shaped
157
+ * GridDataResponse. Server-side IGridService builds the FetchXML via
158
+ * the shared IFetchXmlQueryComposer and runs the query so the client
159
+ * never composes FetchXML directly.
160
+ */
161
+ gridData: `${apiRoot}/grids/data`,
162
+ /**
163
+ * Template — POST a ChartDataRequest (Aggregate / ViewId / FetchXml
164
+ * input modes) and get back a shaped ChartDataResponse. Server-side
165
+ * IChartService builds the FetchXML and runs the query so the
166
+ * client never composes FetchXML directly.
167
+ */
168
+ chartData: `${apiRoot}/charts/data`,
169
+ /** Resolved URL for retrieving metadata for one view. */
170
+ getRetrieveViewMetadataRoute: (viewId) => `${apiRoot}/viewMetadata/${viewId}`,
171
+ /** Resolved URL for retrieving every view metadata record for a table. */
172
+ getRetrieveViewsForTableRoute: (tableLogicalName) => `${apiRoot}/viewMetadata/${encodeURIComponent(tableLogicalName)}`,
173
+ /** Resolved URL for retrieving metadata for one table. Mirrors C# behavior of not encoding the segment. */
174
+ getRetrieveTableMetadataRoute: (tableLogicalName) => `${apiRoot}/tableMetadata/${tableLogicalName}`,
175
+ /**
176
+ * Resolved URL for retrieving the current user's combined
177
+ * `TableSecurityPermission` mask for a single table. Mirrors the
178
+ * server-side `ITablePermissionCache.GetPermissionForUserAsync` lookup
179
+ * the Blazor grid buttons already use via DI — exposed so the React
180
+ * client can read the same answer without firing a grid query.
181
+ * `GridDataResponse.tablePermissions` carries the same value when a
182
+ * grid query happens to be in flight; this endpoint covers the other
183
+ * cases (custom toolbars, conditional UI elsewhere on the page).
184
+ */
185
+ getRetrieveTablePermissionsRoute: (tableLogicalName) => `${apiRoot}/permissions/table/${tableLogicalName}`,
186
+ /** Resolved URL for retrieving localized strings for a culture. */
187
+ getRetrieveLocalizedStringsRoute: (culture) => `${apiRoot}/localizedStrings/${culture}`,
188
+ /** Resolved URL for the localization-bundle manifest endpoint (version + supported locales). */
189
+ getLocalizationBundleManifestRoute: () => `/localizations/version`,
190
+ /**
191
+ * Resolved URL for the per-locale thumbprints endpoint. Returns the
192
+ * content-derived thumbprint for the default bundle plus every loaded
193
+ * table / view at the requested locale. Mirrors C#
194
+ * `Routes.Localizations.GetThumbprintsRoute`.
195
+ */
196
+ getLocalizationThumbprintsRoute: (locale) => `/localizations/${encodeURIComponent(locale)}/thumbprints`,
197
+ /**
198
+ * Resolved URL for the default localization-bundle file (everything outside
199
+ * `tables.*` / `choices.*`) for a given locale + thumbprint. The thumbprint
200
+ * is purely for cache-busting — the server returns the current bundle for
201
+ * the locale regardless of what thumbprint is in the URL. Mirrors C#
202
+ * `Routes.Localizations.GetDefaultBundleRoute`.
203
+ */
204
+ getLocalizationBundleRoute: (locale, thumbprint) => `/localizations/default/${locale}.${thumbprint}.json`,
205
+ /**
206
+ * Resolved URL for the per-table localization-bundle file. Returns every
207
+ * `tables.{name}.*` string for the table plus the global `choices.*`
208
+ * strings any of its columns reference. Mirrors C#
209
+ * `Routes.Localizations.GetTableBundleRoute`.
210
+ */
211
+ getLocalizationTableBundleRoute: (tableName, locale, thumbprint) => `/localizations/tables/${encodeURIComponent(tableName)}/${locale}.${thumbprint}.json`,
212
+ /**
213
+ * Resolved URL for the per-view localization-bundle file. Returns every
214
+ * `tables.{owningTable}.views.{viewId}.*` string. Owning table is resolved
215
+ * server-side from the view id. Mirrors C# `Routes.Localizations.GetViewBundleRoute`.
216
+ */
217
+ getLocalizationViewBundleRoute: (viewId, locale, thumbprint) => `/localizations/views/${viewId}/${locale}.${thumbprint}.json`,
218
+ /** Resolved URL for executing multiple requests. */
219
+ getExecuteMultipleRoute: (returnResponses) => `${apiRoot}/executeMultiple?returnResponses=${returnResponses}`,
220
+ /** Resolved URL for retrieving file info on one record/column. */
221
+ getRetrieveFileInfoRoute: (tableLogicalName, recordId, columnName, includeData) => `${apiRoot}/files/${encodeURIComponent(tableLogicalName)}/${recordId}/${encodeURIComponent(columnName)}?includeData=${includeData}`,
222
+ /** Resolved URL for the batch retrieve-files endpoint. Body is a JSON array of record ids. */
223
+ getRetrieveFilesBatchRoute: (tableLogicalName, columnName, includeData) => `${apiRoot}/files/${encodeURIComponent(tableLogicalName)}/${encodeURIComponent(columnName)}/batch?includeData=${includeData}`,
224
+ /** Resolved URL for clearing one named cache. */
225
+ getClearCacheRoute: (cacheName) => `${apiRoot}/caches/${encodeURIComponent(cacheName)}/clear`,
226
+ /** Table-scoped CRUD routes. */
227
+ tables,
228
+ /** SPA-facing authentication endpoints. */
229
+ auth
230
+ };
231
+ var ui = {
232
+ localization: {
233
+ /** Template — POST to set the culture cookie + redirect. */
234
+ culture: "Culture/{culture}",
235
+ /** Resolved URL for setting the culture cookie. Mirrors C# behavior of not encoding either segment. */
236
+ getCultureRoute: (culture, returnUrl) => `Culture/${culture}?redirectUri=${returnUrl}`
237
+ }
238
+ };
239
+ var Routes = {
240
+ api,
241
+ ui
242
+ };
243
+
244
+ // src/errors.ts
245
+ var PowerPortalsProError = class extends Error {
246
+ status;
247
+ url;
248
+ title;
249
+ detail;
250
+ clrType;
251
+ problem;
252
+ constructor(message, init) {
253
+ super(message);
254
+ this.name = "PowerPortalsProError";
255
+ this.status = init.status;
256
+ this.url = init.url;
257
+ this.title = init.title;
258
+ this.detail = init.detail;
259
+ this.clrType = init.clrType;
260
+ this.problem = init.problem;
261
+ }
262
+ };
263
+ var UnauthorizedAccessError = class extends PowerPortalsProError {
264
+ constructor(message, init) {
265
+ super(message, init);
266
+ this.name = "UnauthorizedAccessError";
267
+ }
268
+ };
269
+ var ArgumentError = class extends PowerPortalsProError {
270
+ constructor(message, init) {
271
+ super(message, init);
272
+ this.name = "ArgumentError";
273
+ }
274
+ };
275
+ var ArgumentNullError = class extends ArgumentError {
276
+ constructor(message, init) {
277
+ super(message, init);
278
+ this.name = "ArgumentNullError";
279
+ }
280
+ };
281
+ var InvalidOperationError = class extends PowerPortalsProError {
282
+ constructor(message, init) {
283
+ super(message, init);
284
+ this.name = "InvalidOperationError";
285
+ }
286
+ };
287
+ function mapProblemDetails(problem, fallbackStatus, url) {
288
+ const message = problem.detail ?? problem.title ?? `${problem.status ?? fallbackStatus} ${problem.title ?? "HTTP error"}${url ? ` on ${url}` : ""}`;
289
+ const init = {
290
+ status: problem.status ?? fallbackStatus,
291
+ url,
292
+ title: problem.title,
293
+ detail: problem.detail,
294
+ clrType: problem.exceptionType,
295
+ problem
296
+ };
297
+ switch (problem.exceptionType) {
298
+ case "System.UnauthorizedAccessException":
299
+ return new UnauthorizedAccessError(message, init);
300
+ case "System.ArgumentNullException":
301
+ return new ArgumentNullError(message, init);
302
+ case "System.ArgumentException":
303
+ return new ArgumentError(message, init);
304
+ case "System.InvalidOperationException":
305
+ return new InvalidOperationError(message, init);
306
+ default:
307
+ return new PowerPortalsProError(message, init);
308
+ }
309
+ }
310
+
311
+ // src/transport.ts
312
+ var Transport = class {
313
+ baseUrl;
314
+ fetchImpl;
315
+ constructor(options = {}) {
316
+ this.baseUrl = (options.baseUrl ?? "").replace(/\/+$/, "");
317
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
318
+ }
319
+ /** Sends a request and returns the parsed JSON response body. */
320
+ async sendJson(method, path, options = {}) {
321
+ const response = await this.send(method, path, options);
322
+ return await response.json();
323
+ }
324
+ /**
325
+ * Sends a request that may return 204 No Content. Returns `null` for the empty case
326
+ * so the caller can branch without inspecting status codes.
327
+ */
328
+ async sendNullableJson(method, path, options = {}) {
329
+ const response = await this.send(method, path, options);
330
+ if (response.status === 204) return null;
331
+ const text = await response.text();
332
+ if (text.length === 0) return null;
333
+ return JSON.parse(text);
334
+ }
335
+ /** Sends a request and discards the response body. */
336
+ async sendVoid(method, path, options = {}) {
337
+ const response = await this.send(method, path, options);
338
+ await response.arrayBuffer().catch(() => void 0);
339
+ }
340
+ /**
341
+ * Low-level send. Throws `PowerPortalsProError` (or a subclass) on non-2xx. Most
342
+ * callers should use {@link sendJson}, {@link sendNullableJson}, or {@link sendVoid}.
343
+ */
344
+ async send(method, path, options = {}) {
345
+ const url = this.baseUrl + path;
346
+ const hasBody = options.body !== void 0;
347
+ const headers = {
348
+ Accept: "application/json",
349
+ ...hasBody ? { "Content-Type": "application/json" } : {},
350
+ ...options.headers ?? {}
351
+ };
352
+ const init = {
353
+ method,
354
+ credentials: "include",
355
+ headers,
356
+ ...hasBody ? { body: JSON.stringify(options.body) } : {},
357
+ ...options.signal ? { signal: options.signal } : {}
358
+ };
359
+ const response = await this.fetchImpl(url, init);
360
+ if (response.ok) return response;
361
+ throw await this.errorFromResponse(response, url);
362
+ }
363
+ async errorFromResponse(response, url) {
364
+ const contentType = response.headers.get("Content-Type") ?? "";
365
+ if (contentType.toLowerCase().includes("application/problem+json")) {
366
+ try {
367
+ const problem = await response.json();
368
+ return mapProblemDetails(problem, response.status, url);
369
+ } catch {
370
+ }
371
+ }
372
+ let detail;
373
+ try {
374
+ const text = await response.text();
375
+ if (text.length > 0) detail = text;
376
+ } catch {
377
+ }
378
+ return new PowerPortalsProError(
379
+ `${response.status} ${response.statusText} on ${url}`,
380
+ {
381
+ status: response.status,
382
+ url,
383
+ title: response.statusText,
384
+ detail,
385
+ clrType: void 0,
386
+ problem: void 0
387
+ }
388
+ );
389
+ }
390
+ };
391
+
392
+ // src/auth-client.ts
393
+ function bodyAndSignal(body, signal) {
394
+ return {
395
+ ...body !== void 0 ? { body } : {},
396
+ ...signal ? { signal } : {}
397
+ };
398
+ }
399
+ var AuthClient = class {
400
+ /**
401
+ * The underlying transport. Exposed so advanced consumers can hit auth endpoints not
402
+ * yet wrapped by a typed method, or share a single transport with a sibling
403
+ * {@link PowerPortalsProClient}.
404
+ */
405
+ transport;
406
+ constructor(transportOrOptions) {
407
+ this.transport = transportOrOptions instanceof Transport ? transportOrOptions : new Transport(transportOrOptions);
408
+ }
409
+ // --- Login + 2FA + logout --------------------------------------------------------
410
+ /**
411
+ * Submits credentials. On success the auth cookie is set and the next
412
+ * {@link getCurrentUserAsync} will report the user as signed in. The returned
413
+ * `result` enum tells the caller whether to navigate (Success), prompt for 2FA
414
+ * (RequiresTwoFactor), or show a generic error (others).
415
+ */
416
+ loginAsync(request, signal) {
417
+ return this.transport.sendJson("POST", Routes.api.auth.login, bodyAndSignal(request, signal));
418
+ }
419
+ /** Submits the 2FA code after a {@link loginAsync} that returned `RequiresTwoFactor`. */
420
+ verifyTwoFactorAsync(request, signal) {
421
+ return this.transport.sendJson("POST", Routes.api.auth.loginTwoFactor, bodyAndSignal(request, signal));
422
+ }
423
+ /** Clears the auth cookie. Idempotent — calling on an already-anonymous session is a no-op. */
424
+ logoutAsync(signal) {
425
+ return this.transport.sendVoid("POST", Routes.api.auth.logout, bodyAndSignal(void 0, signal));
426
+ }
427
+ // --- Registration + email confirmation -------------------------------------------
428
+ /**
429
+ * Creates a new user. When the host configured `RequireConfirmedAccount` (the default),
430
+ * the response carries `RegisterResult.ConfirmationEmailSent` and the user is NOT
431
+ * signed in yet — the client should surface "check your email."
432
+ */
433
+ registerAsync(request, signal) {
434
+ return this.transport.sendJson("POST", Routes.api.auth.register, bodyAndSignal(request, signal));
435
+ }
436
+ /** Confirms a registered email via the registration-link token. */
437
+ confirmEmailAsync(request, signal) {
438
+ return this.transport.sendJson("POST", Routes.api.auth.confirmEmail, bodyAndSignal(request, signal));
439
+ }
440
+ /** Sends a fresh confirmation email. Always 200 — the server doesn't reveal whether the email exists. */
441
+ resendEmailConfirmationAsync(request, signal) {
442
+ return this.transport.sendVoid("POST", Routes.api.auth.resendEmailConfirmation, bodyAndSignal(request, signal));
443
+ }
444
+ // --- Password reset --------------------------------------------------------------
445
+ /** Requests a password-reset email. Always 200 — no enumeration. */
446
+ requestPasswordResetAsync(request, signal) {
447
+ return this.transport.sendVoid("POST", Routes.api.auth.forgotPassword, bodyAndSignal(request, signal));
448
+ }
449
+ /** Completes a password reset using the token from the reset email link. */
450
+ resetPasswordAsync(request, signal) {
451
+ return this.transport.sendJson("POST", Routes.api.auth.resetPassword, bodyAndSignal(request, signal));
452
+ }
453
+ // --- Session ---------------------------------------------------------------------
454
+ /**
455
+ * Snapshot of the current user. Anonymous requests get a non-null `CurrentUserInfo`
456
+ * with `isAuthenticated: false` and the rest of the fields empty — call this on app
457
+ * startup to decide whether to render signed-in UI vs. the login flow.
458
+ */
459
+ getCurrentUserAsync(signal) {
460
+ return this.transport.sendJson("GET", Routes.api.auth.me, bodyAndSignal(void 0, signal));
461
+ }
462
+ /** Lists the configured external (OAuth) login providers (Microsoft, Google, …). */
463
+ getExternalLoginProvidersAsync(signal) {
464
+ return this.transport.sendJson(
465
+ "GET",
466
+ Routes.api.auth.externalProviders,
467
+ bodyAndSignal(void 0, signal)
468
+ );
469
+ }
470
+ // --- External login flow ---------------------------------------------------------
471
+ /**
472
+ * Reads the in-flight external-login cookie set by an OAuth callback. Returns
473
+ * `null` when no callback is in flight (the endpoint replies 204) — the client
474
+ * should redirect back to login in that case.
475
+ */
476
+ getPendingExternalLoginAsync(signal) {
477
+ return this.transport.sendNullableJson(
478
+ "GET",
479
+ Routes.api.auth.externalLoginPending,
480
+ bodyAndSignal(void 0, signal)
481
+ );
482
+ }
483
+ /**
484
+ * Completes an in-flight external login: registers a new local account (or links
485
+ * the external login to an existing account with the same email) and signs the
486
+ * user in.
487
+ */
488
+ confirmExternalLoginAsync(request, signal) {
489
+ return this.transport.sendJson(
490
+ "POST",
491
+ Routes.api.auth.externalLoginConfirm,
492
+ bodyAndSignal(request, signal)
493
+ );
494
+ }
495
+ /**
496
+ * Completes an in-flight external login when the OAuth identity matched more than
497
+ * one portal user (typically a `systemuser` AND a `contact`). Body carries only
498
+ * the `ExternalLoginCandidateKind` the user picked from the chooser; the server
499
+ * re-resolves the underlying record so a malicious caller can't supply an arbitrary id.
500
+ */
501
+ selectExternalLoginAsync(request, signal) {
502
+ return this.transport.sendJson(
503
+ "POST",
504
+ Routes.api.auth.externalLoginSelect,
505
+ bodyAndSignal(request, signal)
506
+ );
507
+ }
508
+ /**
509
+ * Swaps the current auth cookie for the user's alt identity (the contact↔systemuser
510
+ * sibling). The alt id is read from the cookie's claims server-side, so the client
511
+ * supplies no parameters.
512
+ */
513
+ switchIdentityAsync(signal) {
514
+ return this.transport.sendJson(
515
+ "POST",
516
+ Routes.api.auth.switchIdentity,
517
+ bodyAndSignal(void 0, signal)
518
+ );
519
+ }
520
+ // --- Profile ---------------------------------------------------------------------
521
+ /** Reads the signed-in user's manage-profile snapshot (Identity status + linked contact fields). */
522
+ getProfileAsync(signal) {
523
+ return this.transport.sendJson("GET", Routes.api.auth.manage.profile, bodyAndSignal(void 0, signal));
524
+ }
525
+ /** Updates first/last/mobile on the signed-in user's linked Dataverse contact. */
526
+ updateProfileAsync(request, signal) {
527
+ return this.transport.sendVoid("POST", Routes.api.auth.manage.updateProfile, bodyAndSignal(request, signal));
528
+ }
529
+ // --- Password (set + change) -----------------------------------------------------
530
+ /** Adds a local password to an account that doesn't have one (typically external-login-only). */
531
+ setPasswordAsync(request, signal) {
532
+ return this.transport.sendJson("POST", Routes.api.auth.manage.setPassword, bodyAndSignal(request, signal));
533
+ }
534
+ /**
535
+ * Changes the local password (requires the current password). On success the auth
536
+ * cookie is refreshed so other sessions are invalidated by the security stamp rotation.
537
+ */
538
+ changePasswordAsync(request, signal) {
539
+ return this.transport.sendJson(
540
+ "POST",
541
+ Routes.api.auth.manage.changePassword,
542
+ bodyAndSignal(request, signal)
543
+ );
544
+ }
545
+ // --- Email (change + resend confirmation) ----------------------------------------
546
+ /** Starts a change-email flow by sending a confirmation link to the new address. */
547
+ changeEmailAsync(request, signal) {
548
+ return this.transport.sendJson("POST", Routes.api.auth.manage.changeEmail, bodyAndSignal(request, signal));
549
+ }
550
+ /** Re-sends the confirmation link to the user's current (unconfirmed) email. */
551
+ sendEmailConfirmationAsync(signal) {
552
+ return this.transport.sendJson(
553
+ "POST",
554
+ Routes.api.auth.manage.sendEmailConfirmation,
555
+ bodyAndSignal(void 0, signal)
556
+ );
557
+ }
558
+ // --- 2FA management --------------------------------------------------------------
559
+ /** Reads the user's two-factor configuration snapshot (enabled, has authenticator, recovery codes left). */
560
+ getTwoFactorStatusAsync(signal) {
561
+ return this.transport.sendJson(
562
+ "GET",
563
+ Routes.api.auth.manage.twoFactorStatus,
564
+ bodyAndSignal(void 0, signal)
565
+ );
566
+ }
567
+ /** Reads (and creates if missing) the authenticator key + QR-code URI for TOTP enrollment. */
568
+ getAuthenticatorSetupAsync(signal) {
569
+ return this.transport.sendJson(
570
+ "GET",
571
+ Routes.api.auth.manage.authenticatorSetup,
572
+ bodyAndSignal(void 0, signal)
573
+ );
574
+ }
575
+ /**
576
+ * Verifies a code from the authenticator app to finish enabling 2FA. On success and
577
+ * when the user has zero existing recovery codes, a fresh batch is returned in
578
+ * `recoveryCodes`.
579
+ */
580
+ verifyAuthenticatorAsync(request, signal) {
581
+ return this.transport.sendJson(
582
+ "POST",
583
+ Routes.api.auth.manage.verifyAuthenticator,
584
+ bodyAndSignal(request, signal)
585
+ );
586
+ }
587
+ /** Rotates the authenticator key and disables 2FA — the user must re-enroll to re-enable. */
588
+ resetAuthenticatorAsync(signal) {
589
+ return this.transport.sendJson(
590
+ "POST",
591
+ Routes.api.auth.manage.resetAuthenticator,
592
+ bodyAndSignal(void 0, signal)
593
+ );
594
+ }
595
+ /** Disables 2FA on the account. */
596
+ disable2faAsync(signal) {
597
+ return this.transport.sendJson("POST", Routes.api.auth.manage.disable2fa, bodyAndSignal(void 0, signal));
598
+ }
599
+ /** Regenerates recovery codes — invalidates the previous set. The new codes are returned once and only here. */
600
+ generateRecoveryCodesAsync(signal) {
601
+ return this.transport.sendJson(
602
+ "POST",
603
+ Routes.api.auth.manage.generateRecoveryCodes,
604
+ bodyAndSignal(void 0, signal)
605
+ );
606
+ }
607
+ /** Clears this browser's "remember this machine" 2FA cookie. */
608
+ forget2faClientAsync(signal) {
609
+ return this.transport.sendJson(
610
+ "POST",
611
+ Routes.api.auth.manage.forget2faClient,
612
+ bodyAndSignal(void 0, signal)
613
+ );
614
+ }
615
+ // --- External logins management --------------------------------------------------
616
+ /** Lists the user's linked external logins plus available providers to add. */
617
+ getExternalLoginsAsync(signal) {
618
+ return this.transport.sendJson(
619
+ "GET",
620
+ Routes.api.auth.manage.externalLogins,
621
+ bodyAndSignal(void 0, signal)
622
+ );
623
+ }
624
+ /**
625
+ * Reads the combined sign-in-paths snapshot (linked external logins + whether the
626
+ * account has a local password). Used by the manage-external-logins UI to decide
627
+ * whether unlinking a given external login would lock the user out.
628
+ */
629
+ getCurrentLoginInfoAsync(signal) {
630
+ return this.transport.sendJson(
631
+ "GET",
632
+ Routes.api.auth.manage.loginInfo,
633
+ bodyAndSignal(void 0, signal)
634
+ );
635
+ }
636
+ /** Removes one external login from the signed-in user. */
637
+ removeExternalLoginAsync(request, signal) {
638
+ return this.transport.sendJson(
639
+ "POST",
640
+ Routes.api.auth.manage.removeExternalLogin,
641
+ bodyAndSignal(request, signal)
642
+ );
643
+ }
644
+ // --- Personal data ---------------------------------------------------------------
645
+ /** Returns every `[PersonalData]`-marked Identity property + linked external logins. */
646
+ getPersonalDataAsync(signal) {
647
+ return this.transport.sendJson(
648
+ "GET",
649
+ Routes.api.auth.manage.personalData,
650
+ bodyAndSignal(void 0, signal)
651
+ );
652
+ }
653
+ /**
654
+ * Permanently deletes the user's account. When the account has a local password,
655
+ * `request.password` is required — `RequireLocalPassword` in the response means
656
+ * the client should prompt and retry.
657
+ */
658
+ deletePersonalDataAsync(request, signal) {
659
+ return this.transport.sendJson(
660
+ "POST",
661
+ Routes.api.auth.manage.deletePersonalData,
662
+ bodyAndSignal(request, signal)
663
+ );
664
+ }
665
+ };
666
+
667
+ // src/powerportalspro-client.ts
668
+ var PowerPortalsProClient = class {
669
+ /**
670
+ * The underlying transport. Exposed so advanced consumers can hit endpoints not yet
671
+ * wrapped by a typed method (caches, executeMultiple, associate/disassociate, …).
672
+ * Cookie auth, JSON in/out, and problem+json error rehydration apply automatically
673
+ * because the transport owns those concerns.
674
+ */
675
+ transport;
676
+ constructor(transportOrOptions) {
677
+ this.transport = transportOrOptions instanceof Transport ? transportOrOptions : new Transport(transportOrOptions);
678
+ }
679
+ // --- Records (CRUD + FetchXML) ---------------------------------------------------
680
+ /**
681
+ * Creates a new record. URL is derived from `record.tableName`. Server marks all
682
+ * properties as modified before applying so column values arriving from JSON
683
+ * (which carry no dirty flags) round-trip the same way they do from Blazor's
684
+ * dirty-tracked records.
685
+ */
686
+ createRecordAsync(record, signal) {
687
+ return this.transport.sendJson(
688
+ "POST",
689
+ Routes.api.tables.getCreateRoute(record.tableName),
690
+ { body: record, ...signal ? { signal } : {} }
691
+ );
692
+ }
693
+ /**
694
+ * Reads a single record. `columns` (logical names) narrows the projection to a subset;
695
+ * omit it for the table's full set of columns visible to the caller.
696
+ */
697
+ retrieveRecordAsync(tableLogicalName, recordId, columns, signal) {
698
+ return this.transport.sendJson(
699
+ "GET",
700
+ Routes.api.tables.getRetrieveRoute(tableLogicalName, recordId, columns),
701
+ signal ? { signal } : {}
702
+ );
703
+ }
704
+ /**
705
+ * Runs a FetchXML query and returns the matching records plus paging info.
706
+ * The query is the FetchXML XML literal — the same format Dataverse natively accepts.
707
+ */
708
+ retrieveRecordsAsync(fetchXml, signal) {
709
+ return this.transport.sendJson(
710
+ "GET",
711
+ Routes.api.getRetrieveMultipleRoute(fetchXml),
712
+ signal ? { signal } : {}
713
+ );
714
+ }
715
+ /**
716
+ * Loads grid data via the server-side <c>IGridService</c>. The request
717
+ * picks a base query — a stored Dataverse view (<c>viewId</c>) or a
718
+ * caller-supplied FetchXML (<c>fetchXml</c>) — and the server applies
719
+ * the framework's <c>*</c>/<c>%</c> wildcard convention, AND-merges
720
+ * the search filter into the source's existing filter, dispatches to
721
+ * per-column-type search semantics (string <c>like</c>, choice label
722
+ * match → integer <c>in</c>, lookup link-entity + primary-name match,
723
+ * etc.), and projects each column to a {@link ResolvedColumn} so the
724
+ * client can render headers + dispatch cells without a separate
725
+ * metadata round-trip.
726
+ *
727
+ * Exactly one of <c>request.viewId</c> or <c>request.fetchXml</c> must
728
+ * be supplied; sending both or neither returns a 400.
729
+ *
730
+ * Mirrors how the Blazor `IGridService` is consumed —
731
+ * `@powerportalspro/react`'s `useGridData` hook calls this method
732
+ * internally, so most consumers don't invoke it directly.
733
+ */
734
+ loadGridAsync(request, signal) {
735
+ return this.transport.sendJson(
736
+ "POST",
737
+ Routes.api.gridData,
738
+ { body: request, ...signal ? { signal } : {} }
739
+ );
740
+ }
741
+ /**
742
+ * Updates an existing record. URL is derived from `record.tableName` and `record.id` —
743
+ * `record.id` must be set. For a new record, use {@link createRecordAsync} instead.
744
+ */
745
+ updateRecordAsync(record, signal) {
746
+ if (!record.id) {
747
+ throw new Error(
748
+ "PowerPortalsProClient.updateRecordAsync requires record.id. Use createRecordAsync for new records."
749
+ );
750
+ }
751
+ return this.transport.sendJson(
752
+ "PATCH",
753
+ Routes.api.tables.getUpdateRoute(record.tableName, record.id),
754
+ { body: record, ...signal ? { signal } : {} }
755
+ );
756
+ }
757
+ /**
758
+ * Executes a batch of {@link OrganizationRequest}s as a single round-trip,
759
+ * matching Blazor's `IPowerPortalsProService.ExecuteMultipleAsync`. The
760
+ * MainContext save flow uses this to ship every dirty descendant's
761
+ * requests in one shot so a multi-record save lands transactionally
762
+ * server-side.
763
+ *
764
+ * When `returnResponses` is `true` (default), the response array carries
765
+ * one `OrganizationResponse` per request in submission order — useful for
766
+ * `CreateRequest` cases that need the newly-allocated record id. Pass
767
+ * `false` for fire-and-forget batches to skip the response payload.
768
+ */
769
+ executeMultipleAsync(requests, options) {
770
+ const returnResponses = options?.returnResponses ?? true;
771
+ return this.transport.sendJson(
772
+ "POST",
773
+ Routes.api.getExecuteMultipleRoute(returnResponses),
774
+ {
775
+ body: requests,
776
+ ...options?.signal ? { signal: options.signal } : {}
777
+ }
778
+ );
779
+ }
780
+ /** Deletes the record with the given id from the given table. */
781
+ deleteRecordAsync(tableLogicalName, recordId, signal) {
782
+ return this.transport.sendJson(
783
+ "DELETE",
784
+ Routes.api.tables.getDeleteRoute(tableLogicalName, recordId),
785
+ signal ? { signal } : {}
786
+ );
787
+ }
788
+ // --- Charts ----------------------------------------------------------------------
789
+ /**
790
+ * Loads chart data via the server-side <c>IChartService</c>. The
791
+ * request body picks one of three input modes — typed aggregate
792
+ * config, saved-view id, or raw FetchXML — plus the label/value
793
+ * columns to shape the response. Server builds the FetchXML, runs the
794
+ * query, performs the multi-series pivot, and applies combined
795
+ * date-label formatting using the request culture.
796
+ *
797
+ * Mirrors how the Blazor `IChartService` is consumed —
798
+ * `@powerportalspro/react-charts`'s `DataverseChartDataSource` family
799
+ * calls this method internally, so most consumers don't invoke it
800
+ * directly.
801
+ */
802
+ loadChartAsync(request, signal) {
803
+ return this.transport.sendJson(
804
+ "POST",
805
+ Routes.api.chartData,
806
+ { body: request, ...signal ? { signal } : {} }
807
+ );
808
+ }
809
+ // --- Metadata --------------------------------------------------------------------
810
+ /** Reads the metadata describing the columns and relationships of a single Dataverse table. */
811
+ retrieveTableMetadataAsync(tableLogicalName, signal) {
812
+ return this.transport.sendJson(
813
+ "GET",
814
+ Routes.api.getRetrieveTableMetadataRoute(tableLogicalName),
815
+ signal ? { signal } : {}
816
+ );
817
+ }
818
+ /**
819
+ * Returns the current user's combined table-level `TableSecurityPermission`
820
+ * bitmask for `tableLogicalName` — the bitwise union of Read / Create /
821
+ * Write / Delete / Append / AppendTo flags any registered
822
+ * `ITablePermissionHandler` allows for that user on that table. Mirrors
823
+ * the cached `ITablePermissionCache.GetPermissionForUserAsync` lookup
824
+ * Blazor's grid buttons already use directly via DI. Wrapped client-side
825
+ * by the `useTablePermissions(tableName)` React hook with in-flight-promise
826
+ * dedup; reach for the raw client only when you're outside React (e.g.
827
+ * a sample stand-alone script) or need an imperative one-off check.
828
+ *
829
+ * Returns `0` (`TableSecurityPermission.None`) when the user has no
830
+ * permissions on the table; consumers compare via bit math (e.g.
831
+ * `(permissions & TableSecurityPermission.Create) === TableSecurityPermission.Create`).
832
+ */
833
+ retrieveTablePermissionsAsync(tableLogicalName, signal) {
834
+ return this.transport.sendJson(
835
+ "GET",
836
+ Routes.api.getRetrieveTablePermissionsRoute(tableLogicalName),
837
+ signal ? { signal } : {}
838
+ );
839
+ }
840
+ /** Reads metadata for a single view by its Dataverse view id (a GUID). */
841
+ retrieveViewMetadataAsync(viewId, signal) {
842
+ return this.transport.sendJson(
843
+ "GET",
844
+ Routes.api.getRetrieveViewMetadataRoute(viewId),
845
+ signal ? { signal } : {}
846
+ );
847
+ }
848
+ /**
849
+ * Lists every view metadata record for a table — used by view-pickers and default-view
850
+ * resolution. Server-side caching means this is a single Dataverse hit per table per
851
+ * server lifetime.
852
+ */
853
+ retrieveViewsForTableAsync(tableLogicalName, signal) {
854
+ return this.transport.sendJson(
855
+ "GET",
856
+ Routes.api.getRetrieveViewsForTableRoute(tableLogicalName),
857
+ signal ? { signal } : {}
858
+ );
859
+ }
860
+ // --- Localization ----------------------------------------------------------------
861
+ /**
862
+ * Retrieves the version manifest — just the supported-locale codes.
863
+ * Clients fetch this on app startup, resolve which locale to use, then
864
+ * call {@link retrieveLocalizationThumbprintsAsync} for the per-resource
865
+ * thumbprints at that locale. Cached `no-cache` server-side so a new
866
+ * release is detected on next page load.
867
+ */
868
+ retrieveLocalizationBundleManifestAsync(signal) {
869
+ return this.transport.sendJson(
870
+ "GET",
871
+ Routes.api.getLocalizationBundleManifestRoute(),
872
+ signal ? { signal } : {}
873
+ );
874
+ }
875
+ /**
876
+ * Retrieves the per-resource thumbprints for a single locale — the
877
+ * default-bundle thumbprint plus thumbprints for every loaded table / view.
878
+ * Returned from a separate endpoint (vs being baked into the version
879
+ * manifest) so the manifest stays small even at large environment scale.
880
+ * Cached `no-cache` server-side; the per-resource immutable URLs do the
881
+ * heavy caching once these are read.
882
+ */
883
+ retrieveLocalizationThumbprintsAsync(locale, signal) {
884
+ return this.transport.sendJson(
885
+ "GET",
886
+ Routes.api.getLocalizationThumbprintsRoute(locale),
887
+ signal ? { signal } : {}
888
+ );
889
+ }
890
+ /**
891
+ * Resolved URL for the default localization-bundle file at the given
892
+ * locale + thumbprint. Convenience wrapper around the {@link Routes}
893
+ * helper.
894
+ */
895
+ getLocalizationBundleUrl(locale, thumbprint) {
896
+ return Routes.api.getLocalizationBundleRoute(locale, thumbprint);
897
+ }
898
+ /**
899
+ * Fetches the default localization-bundle file (everything outside
900
+ * `tables.*` / `choices.*`) for the given locale + thumbprint. Returns
901
+ * the nested-object shape the source JSON files use
902
+ * (`{ app: { buttons: { … } } }`).
903
+ *
904
+ * Per-table and per-view strings come from
905
+ * {@link retrieveLocalizationTableBundleAsync} and
906
+ * {@link retrieveLocalizationViewBundleAsync} — fetched in parallel by the
907
+ * runtime when source-generator-emitted `tables.{name}` / `views.{viewId}`
908
+ * tokens are passed to the localizer's prefix-load hook.
909
+ *
910
+ * The thumbprint is purely cosmetic (server doesn't validate it). Pass
911
+ * the thumbprint from the manifest so the URL is unique per release and
912
+ * the browser's HTTP cache serves subsequent loads from disk.
913
+ */
914
+ retrieveLocalizationBundleAsync(locale, thumbprint, signal) {
915
+ return this.transport.sendJson(
916
+ "GET",
917
+ Routes.api.getLocalizationBundleRoute(locale, thumbprint),
918
+ signal ? { signal } : {}
919
+ );
920
+ }
921
+ /**
922
+ * Fetches a per-table localization bundle. Returns every `tables.{name}.*`
923
+ * string for the table plus the global `choices.*` strings any of its
924
+ * columns reference. Owning-side resolution: the server reads the table's
925
+ * column metadata to find which global option sets to include.
926
+ *
927
+ * Pass the thumbprint from the matching locale entry of the manifest so
928
+ * the URL is unique per release; the browser caches the response under
929
+ * `Cache-Control: immutable, max-age=31536000`.
930
+ */
931
+ retrieveLocalizationTableBundleAsync(tableName, locale, thumbprint, signal) {
932
+ return this.transport.sendJson(
933
+ "GET",
934
+ Routes.api.getLocalizationTableBundleRoute(tableName, locale, thumbprint),
935
+ signal ? { signal } : {}
936
+ );
937
+ }
938
+ /**
939
+ * Fetches a per-view localization bundle. Returns every
940
+ * `tables.{owningTable}.views.{viewId}.*` string. The owning table is
941
+ * resolved server-side from the view id, so the client only needs to pass
942
+ * the id (lowercased GUID, no braces).
943
+ *
944
+ * Pass the thumbprint from the matching locale entry of the manifest so
945
+ * the URL is unique per release; the browser caches the response under
946
+ * `Cache-Control: immutable, max-age=31536000`.
947
+ */
948
+ retrieveLocalizationViewBundleAsync(viewId, locale, thumbprint, signal) {
949
+ return this.transport.sendJson(
950
+ "GET",
951
+ Routes.api.getLocalizationViewBundleRoute(viewId, locale, thumbprint),
952
+ signal ? { signal } : {}
953
+ );
954
+ }
955
+ /**
956
+ * Retrieves the FULL localized-strings catalog for a culture. The server
957
+ * already merges Dataverse-metadata-derived strings (table / column /
958
+ * choice-option labels) with the framework JSON files and any consumer
959
+ * override files (last-wins). Use this to seed the React-side localizer
960
+ * with one round-trip.
961
+ *
962
+ * Returned shape is a nested object that mirrors the source JSON files —
963
+ * `{ app: { buttons: { save: { label: "Save" } } }, tables: { contact: ... } }`.
964
+ * The client typically flattens this to dotted-key form for `t()` lookup
965
+ * (see {@link flattenStrings} in `@powerportalspro/react`).
966
+ *
967
+ * @param culture culture code matching what server-side ASP.NET request
968
+ * localization recognizes — e.g. `"en"`, `"es"`, `"fr-CA"`. Falls back
969
+ * to the default culture server-side when the value isn't supported.
970
+ */
971
+ retrieveLocalizedStringsAsync(culture, signal) {
972
+ return this.transport.sendJson(
973
+ "GET",
974
+ Routes.api.getRetrieveLocalizedStringsRoute(culture),
975
+ signal ? { signal } : {}
976
+ );
977
+ }
978
+ // --- Files -----------------------------------------------------------------------
979
+ /**
980
+ * Reads file metadata for one record/column pair, optionally including the binary
981
+ * payload. When `includeData` is false (the default), the response carries name,
982
+ * size, and content type but not the bytes — useful for rendering a download link
983
+ * that fetches lazily on click.
984
+ */
985
+ getFileInfoAsync(tableLogicalName, recordId, columnName, includeData = false, signal) {
986
+ return this.transport.sendJson(
987
+ "GET",
988
+ Routes.api.getRetrieveFileInfoRoute(tableLogicalName, recordId, columnName, includeData),
989
+ signal ? { signal } : {}
990
+ );
991
+ }
992
+ /**
993
+ * Batched read for many records of the same table/column — feeds the FileGrid's
994
+ * "Download All" / "Download Selected" flows in one round-trip. Permission checks
995
+ * still apply per-record server-side, so dropped rows (deleted, unauthorized,
996
+ * file-missing) just don't appear in the response.
997
+ */
998
+ getFileInfosAsync(tableLogicalName, recordIds, columnName, includeData = false, signal) {
999
+ return this.transport.sendJson(
1000
+ "POST",
1001
+ Routes.api.getRetrieveFilesBatchRoute(tableLogicalName, columnName, includeData),
1002
+ { body: recordIds, ...signal ? { signal } : {} }
1003
+ );
1004
+ }
1005
+ /**
1006
+ * Builds an archive containing the file payload of every supplied record id for the
1007
+ * named table/column. Replaces the older client-side "fetch each file + zip in the
1008
+ * browser" path with a single server-side step — no base64 round-trip on the wire.
1009
+ *
1010
+ * Two response shapes, selected by `request.responseFormat`:
1011
+ *
1012
+ * - **BinaryStream** (default): the server returns the raw archive bytes; this
1013
+ * method reads them via `response.arrayBuffer()` and wraps the result in a
1014
+ * {@link FileArchiveResult}, deriving `fileName` from the `Content-Disposition`
1015
+ * header and `contentType` from `Content-Type`. Most consumers want this path.
1016
+ * - **Json**: the server returns a {@link FileArchiveJsonResponse} envelope with
1017
+ * the archive bytes base64-encoded. Useful for test harnesses, service workers,
1018
+ * or callers that need to inspect filename/size before triggering a download.
1019
+ *
1020
+ * Either way, the returned `data` is empty (`Uint8Array(0)`) when none of the
1021
+ * supplied records produced a usable file — the caller can surface a "nothing to
1022
+ * download" message instead of streaming an empty archive.
1023
+ */
1024
+ async createFileArchiveAsync(request, signal) {
1025
+ if (request.responseFormat === 1) {
1026
+ const json = await this.transport.sendJson(
1027
+ "POST",
1028
+ Routes.api.createFileArchive,
1029
+ { body: request, ...signal ? { signal } : {} }
1030
+ );
1031
+ return {
1032
+ fileName: json.fileName,
1033
+ contentType: json.contentType,
1034
+ data: base64ToUint8Array(json.data)
1035
+ };
1036
+ }
1037
+ const response = await this.transport.send(
1038
+ "POST",
1039
+ Routes.api.createFileArchive,
1040
+ { body: request, ...signal ? { signal } : {} }
1041
+ );
1042
+ const fileName = parseContentDispositionFileName(response.headers.get("Content-Disposition")) ?? `${request.tableName}-files.zip`;
1043
+ const contentType = response.headers.get("Content-Type")?.split(";")[0]?.trim() ?? "application/octet-stream";
1044
+ const buffer = await response.arrayBuffer();
1045
+ return { fileName, contentType, data: new Uint8Array(buffer) };
1046
+ }
1047
+ /**
1048
+ * Reads the environment-wide file upload settings (blocked extension list, max
1049
+ * upload size in KB). UI consumers use this to validate uploads client-side before
1050
+ * the round-trip, mirroring what the server enforces.
1051
+ */
1052
+ getEnvironmentFileSettingsAsync(signal) {
1053
+ return this.transport.sendJson(
1054
+ "GET",
1055
+ Routes.api.environmentFileSettings,
1056
+ signal ? { signal } : {}
1057
+ );
1058
+ }
1059
+ };
1060
+ function base64ToUint8Array(base64) {
1061
+ const binary = atob(base64);
1062
+ const bytes = new Uint8Array(binary.length);
1063
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1064
+ return bytes;
1065
+ }
1066
+ function parseContentDispositionFileName(header) {
1067
+ if (!header) return null;
1068
+ const star = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(header);
1069
+ if (star?.[1]) {
1070
+ try {
1071
+ return decodeURIComponent(star[1]);
1072
+ } catch {
1073
+ }
1074
+ }
1075
+ const plain = /filename="?([^";]+)"?/i.exec(header);
1076
+ return plain?.[1] ?? null;
1077
+ }
1078
+
1079
+ // src/generated/result-codes.ts
1080
+ var AggregateType = {
1081
+ None: 0,
1082
+ Count: 1,
1083
+ CountColumn: 2,
1084
+ Sum: 3,
1085
+ Avg: 4,
1086
+ Min: 5,
1087
+ Max: 6
1088
+ };
1089
+ var ArchiveFormat = {
1090
+ Zip: 0
1091
+ };
1092
+ var ChangeEmailResult = {
1093
+ ConfirmationEmailSent: 0,
1094
+ SameAsCurrentEmail: 1
1095
+ };
1096
+ var ChangePasswordResult = {
1097
+ Success: 0,
1098
+ IncorrectOldPassword: 1,
1099
+ InvalidPassword: 2
1100
+ };
1101
+ var ChartDateGrouping = {
1102
+ None: 0,
1103
+ Day: 1,
1104
+ Week: 2,
1105
+ Month: 3,
1106
+ Quarter: 4,
1107
+ Year: 5,
1108
+ MonthAndYear: 6,
1109
+ DayAndMonth: 7,
1110
+ DayAndMonthAndYear: 8,
1111
+ WeekAndYear: 9,
1112
+ QuarterAndYear: 10
1113
+ };
1114
+ var ColumnType = {
1115
+ Boolean: 0,
1116
+ Customer: 1,
1117
+ DateTime: 2,
1118
+ Decimal: 3,
1119
+ Double: 4,
1120
+ Integer: 5,
1121
+ Lookup: 6,
1122
+ Memo: 7,
1123
+ Money: 8,
1124
+ Owner: 9,
1125
+ PartyList: 10,
1126
+ Choice: 11,
1127
+ State: 12,
1128
+ Status: 13,
1129
+ String: 14,
1130
+ Uniqueidentifier: 15,
1131
+ CalendarRules: 16,
1132
+ Virtual: 17,
1133
+ BigInt: 18,
1134
+ ManagedProperty: 19,
1135
+ EntityName: 20,
1136
+ MultiSelectChoice: 40,
1137
+ File: 41,
1138
+ Image: 42
1139
+ };
1140
+ var ConfirmExternalLoginResult = {
1141
+ SignedIn: 0,
1142
+ ConfirmationEmailSent: 1,
1143
+ NoPendingExternalLogin: 2,
1144
+ Failure: 3
1145
+ };
1146
+ var DateTimeBehavior = {
1147
+ DateOnly: 1,
1148
+ UserLocal: 2,
1149
+ TimeZoneIndependent: 3
1150
+ };
1151
+ var DeletePersonalDataResult = {
1152
+ Success: 0,
1153
+ IncorrectPassword: 1,
1154
+ RequireLocalPassword: 2
1155
+ };
1156
+ var ExternalLoginCandidateKind = {
1157
+ Contact: 0,
1158
+ SystemUser: 1
1159
+ };
1160
+ var FileArchiveResponseFormat = {
1161
+ BinaryStream: 0,
1162
+ Json: 1
1163
+ };
1164
+ var JoinOperator = {
1165
+ Inner: 0,
1166
+ LeftOuter: 1,
1167
+ Natural: 2,
1168
+ MatchFirstRowUsingCrossApply: 3,
1169
+ In: 4,
1170
+ Exists: 5,
1171
+ Any: 6,
1172
+ NotAny: 7,
1173
+ All: 8,
1174
+ NotAll: 9
1175
+ };
1176
+ var LoginResult = {
1177
+ Success: 0,
1178
+ RequiresTwoFactor: 1,
1179
+ InvalidCredentials: 2,
1180
+ EmailNotConfirmed: 3,
1181
+ LockedOut: 4
1182
+ };
1183
+ var RegisterResult = {
1184
+ ConfirmationEmailSent: 0,
1185
+ SignedIn: 1,
1186
+ EmailAlreadyInUse: 2
1187
+ };
1188
+ var RelationshipFilterMode = {
1189
+ IncludeExistingRecords: 0,
1190
+ ExcludeExistingRecords: 1
1191
+ };
1192
+ var RequiredLevel = {
1193
+ None: 0,
1194
+ SystemRequired: 1,
1195
+ ApplicationRequired: 2,
1196
+ Recommended: 3
1197
+ };
1198
+ var ResetPasswordResult = {
1199
+ Success: 0,
1200
+ InvalidOrExpiredToken: 1,
1201
+ InvalidPassword: 2
1202
+ };
1203
+ var SwitchIdentityResult = {
1204
+ Switched: 0,
1205
+ NoAltIdentity: 1,
1206
+ AltIdentityNotFound: 2,
1207
+ NotAuthenticated: 3
1208
+ };
1209
+ var TableSecurityPermission = {
1210
+ None: 0,
1211
+ Read: 1,
1212
+ Create: 2,
1213
+ Write: 4,
1214
+ Delete: 8,
1215
+ Append: 16,
1216
+ AppendTo: 32,
1217
+ All: 63
1218
+ };
1219
+
1220
+ // src/record-id.ts
1221
+ var EMPTY_GUID = "00000000-0000-0000-0000-000000000000";
1222
+ function getRecordReferenceId(record) {
1223
+ if (!record) return void 0;
1224
+ if (record.id && record.id !== EMPTY_GUID) return record.id;
1225
+ return record._idForCreate ?? void 0;
1226
+ }
1227
+
1228
+ // src/index.ts
1229
+ var VERSION = "0.1.0";
1230
+
1231
+ exports.AggregateType = AggregateType;
1232
+ exports.ArchiveFormat = ArchiveFormat;
1233
+ exports.ArgumentError = ArgumentError;
1234
+ exports.ArgumentNullError = ArgumentNullError;
1235
+ exports.AuthClient = AuthClient;
1236
+ exports.ChangeEmailResult = ChangeEmailResult;
1237
+ exports.ChangePasswordResult = ChangePasswordResult;
1238
+ exports.ChartDateGrouping = ChartDateGrouping;
1239
+ exports.ColumnType = ColumnType;
1240
+ exports.ConfirmExternalLoginResult = ConfirmExternalLoginResult;
1241
+ exports.DateTimeBehavior = DateTimeBehavior;
1242
+ exports.DeletePersonalDataResult = DeletePersonalDataResult;
1243
+ exports.ExternalLoginCandidateKind = ExternalLoginCandidateKind;
1244
+ exports.FileArchiveResponseFormat = FileArchiveResponseFormat;
1245
+ exports.InvalidOperationError = InvalidOperationError;
1246
+ exports.JoinOperator = JoinOperator;
1247
+ exports.LoginResult = LoginResult;
1248
+ exports.PowerPortalsProClient = PowerPortalsProClient;
1249
+ exports.PowerPortalsProError = PowerPortalsProError;
1250
+ exports.RegisterResult = RegisterResult;
1251
+ exports.RelationshipFilterMode = RelationshipFilterMode;
1252
+ exports.RequiredLevel = RequiredLevel;
1253
+ exports.ResetPasswordResult = ResetPasswordResult;
1254
+ exports.Routes = Routes;
1255
+ exports.SwitchIdentityResult = SwitchIdentityResult;
1256
+ exports.TableSecurityPermission = TableSecurityPermission;
1257
+ exports.Transport = Transport;
1258
+ exports.UnauthorizedAccessError = UnauthorizedAccessError;
1259
+ exports.VERSION = VERSION;
1260
+ exports.getRecordReferenceId = getRecordReferenceId;
1261
+ exports.mapProblemDetails = mapProblemDetails;
1262
+ //# sourceMappingURL=index.cjs.map
1263
+ //# sourceMappingURL=index.cjs.map