@saasicat/ui-vue 0.24.2 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +32 -8
  2. package/dist/{catalog-DJcfm5N2.d.cts → catalog-Dch1Ryw0.d.cts} +353 -69
  3. package/dist/{catalog-DJcfm5N2.d.ts → catalog-Dch1Ryw0.d.ts} +353 -69
  4. package/dist/{chunk-F7NRM6KI.js → chunk-BPF2BMCQ.js} +748 -24
  5. package/dist/{chunk-D74FFCCP.js → chunk-NEXTZZRQ.js} +95 -177
  6. package/dist/{chunk-TQQMKWN6.js → chunk-O3J3ITF2.js} +119 -7
  7. package/dist/client/index.cjs +758 -83
  8. package/dist/client/index.d.cts +414 -9
  9. package/dist/client/index.d.ts +414 -9
  10. package/dist/client/index.js +84 -12
  11. package/dist/index.cjs +1325 -294
  12. package/dist/index.d.cts +205 -15
  13. package/dist/index.d.ts +205 -15
  14. package/dist/index.js +566 -240
  15. package/dist/quasar/index.cjs +574 -62
  16. package/dist/quasar/index.d.cts +61 -6
  17. package/dist/quasar/index.d.ts +61 -6
  18. package/dist/quasar/index.js +83 -19
  19. package/dist/{use-sa-theme-OUBlaqgl.d.ts → resource-registry-D7Xjs-5f.d.cts} +157 -7
  20. package/dist/{use-sa-theme-B5t7oXph.d.cts → resource-registry-TaR7rtq0.d.ts} +157 -7
  21. package/package.json +4 -3
  22. package/src/client/admin-error.ts +552 -0
  23. package/src/client/admin-resource-client.ts +9 -12
  24. package/src/client/batch-column-fetcher.ts +2 -2
  25. package/src/client/boot-loader.ts +12 -1
  26. package/src/client/http/axios-http-client.ts +383 -0
  27. package/src/client/http/fetch-http-client.ts +89 -0
  28. package/src/client/http/index.ts +6 -0
  29. package/src/client/http-json.ts +82 -16
  30. package/src/client/i18n/messages/discovery.ts +0 -6
  31. package/src/client/i18n/messages/errors.ts +44 -0
  32. package/src/client/i18n/messages/marketing.ts +0 -6
  33. package/src/client/i18n/messages/promos.ts +0 -2
  34. package/src/client/i18n/messages/shell.ts +12 -0
  35. package/src/client/i18n/messages.ts +3 -0
  36. package/src/client/index.ts +6 -0
  37. package/src/client/manifest-loader.ts +65 -15
  38. package/src/client/resources/audit.resource.ts +25 -0
  39. package/src/client/resources/define-resource.ts +166 -0
  40. package/src/client/resources/index.ts +42 -0
  41. package/src/client/resources/list-resource.ts +176 -0
  42. package/src/client/resources/plans.resource.ts +169 -0
  43. package/src/client/resources/resource-request.ts +117 -0
  44. package/src/client/resources/tenants.resource.ts +27 -0
  45. package/src/client/types.ts +10 -3
  46. package/src/components/BundleVersionPublishDialog.vue +1 -1
  47. package/src/components/MarketingPromotionsTab.vue +263 -241
  48. package/src/components/ThemeSwitcher.vue +85 -0
  49. package/src/components/admin-page/AdminAccordion.vue +125 -0
  50. package/src/components/bundle-editor/BundleCreatePanel.vue +1 -1
  51. package/src/components/dialogs/PromoCodeDialogFields.vue +20 -20
  52. package/src/components/plan/PlanCycleToggle.vue +1 -1
  53. package/src/components/plan-detail/PlanVersionsPanel.vue +5 -2
  54. package/src/index.ts +5 -0
  55. package/src/pages-standard/AdminLayout.vue +57 -0
  56. package/src/pages-standard/AdminManifestErrorPage.vue +4 -3
  57. package/src/pages-standard/BundlesPage.vue +5 -35
  58. package/src/pages-standard/DiscoveryPage.vue +8 -1
  59. package/src/pages-standard/MarketingCatalogPage.vue +14 -3
  60. package/src/pages-standard/SuperAdminLoginPage.vue +23 -2
  61. package/src/pages-standard/SuperAdminSetupWizard.vue +37 -3
  62. package/src/pages-standard/bundles-page/BundleAccordionList.vue +30 -32
  63. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +80 -88
  64. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +73 -79
  65. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +44 -6
  66. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +8 -2
  67. package/src/pages-tenant/PackageSnapshotPanel.vue +31 -2
  68. package/src/quasar/confirm.ts +83 -0
  69. package/src/quasar/create-super-admin-app.ts +84 -10
  70. package/src/quasar/dark-bridge.ts +47 -10
  71. package/src/quasar/index.ts +1 -0
  72. package/src/ui/theme/components/accordion.css +118 -0
  73. package/src/ui/theme/index.css +1 -0
  74. package/src/vue/create-admin-routes.ts +9 -1
  75. package/src/vue/platform-loaders.ts +12 -2
  76. package/src/vue/resource-registry.ts +211 -0
  77. package/src/vue/super-admin-context.ts +20 -5
  78. package/src/vue/ui-confirm.ts +76 -0
  79. package/src/vue/use-api-list.ts +50 -45
  80. package/src/vue/use-async-action.ts +170 -0
  81. package/src/vue/use-async-data.ts +83 -0
  82. package/src/vue/use-bundles.ts +34 -7
  83. package/src/vue/use-catalog-entries.ts +37 -12
  84. package/src/vue/use-discovery.ts +27 -6
  85. package/src/vue/use-marketing-projections.ts +29 -7
  86. package/src/vue/use-plans.ts +36 -8
  87. package/src/vue/use-promotions.ts +27 -7
  88. package/src/vue/use-resource-list.ts +272 -0
  89. package/src/vue/use-sa-theme.ts +34 -5
  90. package/src/vue/use-tenant-subscription-bundles.ts +22 -3
package/dist/index.cjs CHANGED
@@ -24,6 +24,7 @@ __export(src_exports, {
24
24
  ALL_ROWS: () => ALL_ROWS,
25
25
  ActionDefNotInManifestError: () => ActionDefNotInManifestError,
26
26
  ActionRegistry: () => ActionRegistry,
27
+ AdminError: () => AdminError,
27
28
  BatchColumnDriftError: () => BatchColumnDriftError,
28
29
  BatchColumnFetcher: () => BatchColumnFetcher,
29
30
  BootLoadError: () => BootLoadError,
@@ -44,6 +45,10 @@ __export(src_exports, {
44
45
  IDENTITY_ACCENT_VALUES: () => IDENTITY_ACCENT_VALUES,
45
46
  IDENTITY_NEUTRAL: () => IDENTITY_NEUTRAL,
46
47
  IDENTITY_NEUTRAL_VALUE: () => IDENTITY_NEUTRAL_VALUE,
48
+ LIST_FIRST_PAGE: () => LIST_FIRST_PAGE,
49
+ LIST_PAGE_SIZE_DEFAULT: () => LIST_PAGE_SIZE_DEFAULT,
50
+ LIST_PAGE_SIZE_MAX: () => LIST_PAGE_SIZE_MAX,
51
+ LIST_PAGINATION_PARAMS: () => LIST_PAGINATION_PARAMS,
47
52
  ManifestLoadError: () => ManifestLoadError,
48
53
  ManifestLoader: () => ManifestLoader,
49
54
  MarketingProjectionsApiError: () => MarketingProjectionsApiError,
@@ -53,15 +58,18 @@ __export(src_exports, {
53
58
  PlansApiError: () => PlansApiError,
54
59
  ProjectPageHost: () => ProjectPageHost,
55
60
  PromotionsApiError: () => PromotionsApiError,
61
+ SA_COLOR_SCHEMES: () => SA_COLOR_SCHEMES,
56
62
  SA_INTL_LOCALES: () => SA_INTL_LOCALES,
57
63
  SA_LOCALES: () => SA_LOCALES,
58
64
  SA_LOCALE_LABELS: () => SA_LOCALE_LABELS,
59
65
  SA_LOCALE_STORAGE_KEY: () => SA_LOCALE_STORAGE_KEY,
60
66
  SA_MESSAGES: () => SA_MESSAGES,
61
67
  SA_THEME_KEY: () => SA_THEME_KEY,
68
+ SA_THEME_STORAGE_KEY: () => SA_THEME_STORAGE_KEY,
62
69
  SERVER_PAGE_SIZE_OPTIONS: () => SERVER_PAGE_SIZE_OPTIONS,
63
70
  SUPER_ADMIN_ACTIONS_KEY: () => SUPER_ADMIN_ACTIONS_KEY,
64
71
  SUPER_ADMIN_BRAND_KEY: () => SUPER_ADMIN_BRAND_KEY,
72
+ SUPER_ADMIN_CONFIRM_KEY: () => SUPER_ADMIN_CONFIRM_KEY,
65
73
  SUPER_ADMIN_ENDPOINTS_KEY: () => SUPER_ADMIN_ENDPOINTS_KEY,
66
74
  SUPER_ADMIN_EXTENSIONS_KEY: () => SUPER_ADMIN_EXTENSIONS_KEY,
67
75
  SUPER_ADMIN_HTTP_KEY: () => SUPER_ADMIN_HTTP_KEY,
@@ -70,19 +78,28 @@ __export(src_exports, {
70
78
  SUPER_ADMIN_MANIFEST_CLEAR_CACHE_KEY: () => SUPER_ADMIN_MANIFEST_CLEAR_CACHE_KEY,
71
79
  SUPER_ADMIN_MANIFEST_KEY: () => SUPER_ADMIN_MANIFEST_KEY,
72
80
  SUPER_ADMIN_NOTIFY_KEY: () => SUPER_ADMIN_NOTIFY_KEY,
81
+ SUPER_ADMIN_RESOURCES_KEY: () => SUPER_ADMIN_RESOURCES_KEY,
73
82
  TenantSubscriptionBundlesApiError: () => TenantSubscriptionBundlesApiError,
83
+ adminErrorMessage: () => adminErrorMessage,
84
+ auditResource: () => auditResource,
85
+ bindResource: () => bindResource,
74
86
  buildFeatureRegistry: () => buildFeatureRegistry,
75
87
  buildFeatureRouterGuard: () => buildFeatureRouterGuard,
76
88
  buildNavigationGuard: () => buildNavigationGuard,
77
89
  buildQuotaRegistry: () => buildQuotaRegistry,
78
90
  buildRoutes: () => buildRoutes,
79
91
  buildSidebar: () => buildSidebar,
92
+ clampListPage: () => clampListPage,
93
+ clampListPageSize: () => clampListPageSize,
80
94
  countPlans: () => countPlans,
81
95
  createAdminResourceClient: () => createAdminResourceClient,
82
96
  createAdminRoutes: () => createAdminRoutes,
97
+ createAxiosHttpClient: () => createAxiosHttpClient,
98
+ createFetchHttpClient: () => createFetchHttpClient,
83
99
  createManifestStore: () => createManifestStore,
84
100
  createPlatformLoaders: () => createPlatformLoaders,
85
101
  createProjectPageHostRoute: () => createProjectPageHostRoute,
102
+ createResourceRegistry: () => createResourceRegistry,
86
103
  createSaCatalog: () => createSaCatalog,
87
104
  createSaTheme: () => createSaTheme,
88
105
  createSuperAdminI18n: () => createSuperAdminI18n,
@@ -92,30 +109,57 @@ __export(src_exports, {
92
109
  defaultSectionOrder: () => defaultSectionOrder,
93
110
  defaultTenantPlanSectionI18n: () => defaultTenantPlanSectionI18n,
94
111
  defaultToneForActionKey: () => defaultToneForActionKey,
112
+ defineListOp: () => defineListOp,
95
113
  defineMessages: () => defineMessages,
114
+ defineResource: () => defineResource,
115
+ filterQueryString: () => filterQueryString,
96
116
  formatCurrency: () => formatCurrency,
97
117
  formatMessage: () => formatMessage,
98
118
  getJson: () => getJson,
99
119
  identityAccentAt: () => identityAccentAt,
100
120
  identityAccentFor: () => identityAccentFor,
101
121
  identityChipStyle: () => identityChipStyle,
122
+ isAdminError: () => isAdminError,
123
+ isAxiosNoResponseError: () => isAxiosNoResponseError,
102
124
  isCurrentlyValid: () => isCurrentlyValid,
125
+ isEmptyResponse: () => isEmptyResponse,
103
126
  isExpired: () => isExpired,
104
127
  isFutureScheduled: () => isFutureScheduled,
128
+ isPlatformError: () => isPlatformError,
105
129
  isProductionBoot: () => isProductionBoot,
106
130
  isSaBuiltinLocale: () => isSaBuiltinLocale,
131
+ isSentInQuery: () => isSentInQuery,
132
+ isTransportFailure: () => isTransportFailure,
133
+ listUrl: () => listUrl,
134
+ markEmptyResponse: () => markEmptyResponse,
135
+ markPlatformError: () => markPlatformError,
136
+ markTransportFailure: () => markTransportFailure,
107
137
  mergeMessages: () => mergeMessages,
108
138
  planChangeWizardI18n: () => planChangeWizardI18n,
139
+ planVersionsResource: () => planVersionsResource,
140
+ plansResource: () => plansResource,
141
+ platformResources: () => platformResources,
109
142
  postJson: () => postJson,
110
143
  provideEntitlement: () => provideEntitlement,
144
+ readErrorCode: () => readErrorCode,
145
+ readErrorDetail: () => readErrorDetail,
146
+ readListPage: () => readListPage,
147
+ requestJson: () => requestJson,
148
+ requestJsonBody: () => requestJsonBody,
149
+ requireServerAnswer: () => requireServerAnswer,
111
150
  resolveExtension: () => resolveExtension,
112
151
  resolveLoginBranding: () => resolveLoginBranding,
113
152
  resolveMessages: () => resolveMessages,
114
153
  resolvePlans: () => resolvePlans,
154
+ resolveSuperAdminEndpoints: () => resolveSuperAdminEndpoints,
155
+ tenantsResource: () => tenantsResource,
156
+ toAdminError: () => toAdminError,
115
157
  todayIsoDate: () => todayIsoDate,
116
158
  trimTrailingSlashes: () => trimTrailingSlashes,
117
159
  useActions: () => useActions,
118
160
  useApiList: () => useApiList,
161
+ useAsyncAction: () => useAsyncAction,
162
+ useAsyncData: () => useAsyncData,
119
163
  useAuditEntries: () => useAuditEntries,
120
164
  useBatchColumns: () => useBatchColumns,
121
165
  useBulkPublish: () => useBulkPublish,
@@ -137,6 +181,8 @@ __export(src_exports, {
137
181
  usePlatformTenantActions: () => usePlatformTenantActions,
138
182
  usePromotions: () => usePromotions,
139
183
  usePublicBoot: () => usePublicBoot,
184
+ useResource: () => useResource,
185
+ useResourceList: () => useResourceList,
140
186
  useSaMessages: () => useSaMessages,
141
187
  useSaTheme: () => useSaTheme,
142
188
  useSubscriptionDraft: () => useSubscriptionDraft,
@@ -160,6 +206,287 @@ module.exports = __toCommonJS(src_exports);
160
206
  // src/client/version.ts
161
207
  var ADMIN_UI_VERSION = "1.2.0";
162
208
 
209
+ // src/client/i18n/format.ts
210
+ var import_types = require("@saasicat/types");
211
+ function formatMessage(template, params) {
212
+ return (0, import_types.formatErrorMessage)(template, params);
213
+ }
214
+
215
+ // src/client/admin-error.ts
216
+ var ADMIN_ERROR = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/AdminError");
217
+ var PLATFORM_ERROR = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/PlatformError");
218
+ function markPlatformError(error) {
219
+ Object.defineProperty(error, PLATFORM_ERROR, { value: true });
220
+ }
221
+ function isPlatformError(value) {
222
+ return typeof value === "object" && value !== null && PLATFORM_ERROR in value;
223
+ }
224
+ var EMPTY_RESPONSE = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/EmptyResponse");
225
+ function markEmptyResponse(error) {
226
+ Object.defineProperty(error, EMPTY_RESPONSE, { value: true });
227
+ return error;
228
+ }
229
+ function isEmptyResponse(value) {
230
+ return typeof value === "object" && value !== null && EMPTY_RESPONSE in value;
231
+ }
232
+ var TRANSPORT_FAILURE = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/TransportFailure");
233
+ function markTransportFailure(error) {
234
+ if (typeof error === "object" && error !== null) {
235
+ try {
236
+ Object.defineProperty(error, TRANSPORT_FAILURE, { value: true });
237
+ } catch {
238
+ }
239
+ }
240
+ return error;
241
+ }
242
+ function isTransportFailure(value) {
243
+ return typeof value === "object" && value !== null && TRANSPORT_FAILURE in value;
244
+ }
245
+ function describe(init) {
246
+ const status = init.status ?? 0;
247
+ const where = [init.method, init.url].filter(Boolean).join(" ");
248
+ const what = status > 0 ? `HTTP ${status}` : "request failed";
249
+ const head = where ? `${where} \u2192 ${what}` : what;
250
+ const code = init.code ? ` (${init.code})` : "";
251
+ return init.detail ? `${head}${code}: ${init.detail}` : `${head}${code}`;
252
+ }
253
+ var AdminError = class extends Error {
254
+ /** HTTP status. `0` means the request never produced one. */
255
+ status;
256
+ /** Machine-readable code from the response body, when it carried one. */
257
+ code;
258
+ /** Parsed response body, when there was one. */
259
+ body;
260
+ /** Requested URL, when known. */
261
+ url;
262
+ /** Request method, when known. */
263
+ method;
264
+ /**
265
+ * Text the failing side supplied — the only candidate for user-facing
266
+ * output. `undefined` when nothing was supplied.
267
+ */
268
+ detail;
269
+ /** The request completed but the answer was unusable. See `AdminErrorInit`. */
270
+ emptyResponse;
271
+ /** The request never reached the server. See `AdminErrorInit`. */
272
+ transportFailure;
273
+ constructor(init = {}) {
274
+ super(init.message ?? describe(init), { cause: init.cause });
275
+ this.name = "AdminError";
276
+ this.status = init.status ?? 0;
277
+ this.code = init.code;
278
+ this.body = init.body;
279
+ this.url = init.url;
280
+ this.method = init.method;
281
+ this.detail = init.detail;
282
+ this.emptyResponse = init.emptyResponse ?? false;
283
+ this.transportFailure = init.transportFailure ?? false;
284
+ Object.defineProperty(this, ADMIN_ERROR, { value: true });
285
+ markPlatformError(this);
286
+ }
287
+ };
288
+ function isAdminError(err) {
289
+ return typeof err === "object" && err !== null && ADMIN_ERROR in err;
290
+ }
291
+ function asRecord(value) {
292
+ return typeof value === "object" && value !== null ? value : void 0;
293
+ }
294
+ function asString(value) {
295
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
296
+ }
297
+ function isAxiosNoResponseError(value) {
298
+ const record = asRecord(value);
299
+ if (!record) return false;
300
+ return record.isAxiosError === true && record.request != null && record.response == null;
301
+ }
302
+ function readErrorDetail(body) {
303
+ const record = asRecord(body);
304
+ if (!record) return asString(body);
305
+ const message = record.message;
306
+ if (Array.isArray(message)) {
307
+ const parts = message.filter((part) => typeof part === "string");
308
+ return parts.length > 0 ? parts.join(", ") : void 0;
309
+ }
310
+ return asString(message) ?? asString(record.error) ?? asString(record.reason);
311
+ }
312
+ function readErrorCode(body) {
313
+ return asString(asRecord(body)?.code);
314
+ }
315
+ function toAdminError(err) {
316
+ if (isAdminError(err)) return err;
317
+ const record = asRecord(err);
318
+ const response = asRecord(record?.response);
319
+ if (response && typeof response.status === "number") {
320
+ const config = asRecord(record?.config);
321
+ const body = response.data;
322
+ return new AdminError({
323
+ status: response.status,
324
+ code: readErrorCode(body),
325
+ body,
326
+ url: asString(config?.url),
327
+ method: asString(config?.method)?.toUpperCase(),
328
+ // Only what the response actually said. `err.message` here is
329
+ // axios's own generated line ("Request failed with status code
330
+ // 401") — English, untranslated, and about the transport rather
331
+ // than about what went wrong. Letting it through as `detail` would
332
+ // make `adminErrorMessage` return it in preference to the
333
+ // localized wording, which is the whole point of that wording.
334
+ detail: readErrorDetail(body),
335
+ cause: err
336
+ });
337
+ }
338
+ if (record && typeof record.status === "number") {
339
+ const ours = isPlatformError(err);
340
+ return new AdminError({
341
+ status: record.status,
342
+ code: readErrorCode(record.body) ?? asString(record.code),
343
+ body: record.body,
344
+ // Same reasoning as above: `BundlesApiError`'s message is
345
+ // "Bundles API responded with HTTP 403", a diagnostic. It stays on
346
+ // `message`, where logs read it.
347
+ detail: readErrorDetail(record.body) ?? (ours ? void 0 : asString(record.message)),
348
+ // Asked of the throw site, not of the class: only the site that
349
+ // read the response knows whether one arrived. `status === 0 &&
350
+ // ours` was the same question put to the wrong witness — the
351
+ // loaders raise branded errors from a status the client chose, so
352
+ // a client that resolves a transport failure as `0` turned a
353
+ // failed GET into "the change may have been applied".
354
+ emptyResponse: isEmptyResponse(err),
355
+ transportFailure: isTransportFailure(err),
356
+ message: asString(record.message),
357
+ cause: err
358
+ });
359
+ }
360
+ if (err instanceof Error) {
361
+ const transport = isTransportFailure(err) || isAxiosNoResponseError(err);
362
+ const saidSomethingReadable = !transport && !(err instanceof TypeError);
363
+ return new AdminError({
364
+ detail: saidSomethingReadable ? asString(err.message) : void 0,
365
+ transportFailure: transport,
366
+ message: err.message,
367
+ cause: err
368
+ });
369
+ }
370
+ const text = asString(err) ?? asString(record?.message);
371
+ return new AdminError({
372
+ code: asString(record?.code),
373
+ detail: text,
374
+ message: text,
375
+ cause: err
376
+ });
377
+ }
378
+ function statusKey(status) {
379
+ if (status === 400 || status === 422) return "validation";
380
+ if (status === 401) return "unauthorized";
381
+ if (status === 403) return "forbidden";
382
+ if (status === 404) return "notFound";
383
+ if (status === 409) return "conflict";
384
+ if (status === 429) return "rateLimited";
385
+ if (status >= 500) return "server";
386
+ return void 0;
387
+ }
388
+ function adminErrorMessage(err, msgs) {
389
+ const error = toAdminError(err);
390
+ if (error.detail) return error.detail;
391
+ if (error.emptyResponse) return msgs.emptyResponse;
392
+ if (error.transportFailure) return msgs.network;
393
+ if (error.status > 0) {
394
+ const key = statusKey(error.status);
395
+ return key ? msgs[key] : formatMessage(msgs.httpStatus, { status: error.status });
396
+ }
397
+ return msgs.unexpected;
398
+ }
399
+
400
+ // src/client/http-json.ts
401
+ function serverAnswered(status) {
402
+ return status > 0;
403
+ }
404
+ function requireServerAnswer(status, method, url, raise) {
405
+ if (serverAnswered(status)) return;
406
+ throw markTransportFailure(
407
+ raise(`${method} ${url} produced no HTTP status \u2014 the request did not reach the server`)
408
+ );
409
+ }
410
+ var HttpJsonError = AdminError;
411
+ async function readErrorBody(res) {
412
+ try {
413
+ return await res.json();
414
+ } catch {
415
+ return void 0;
416
+ }
417
+ }
418
+ async function failed(res, method, url) {
419
+ const body = await readErrorBody(res);
420
+ return new AdminError({
421
+ status: res.status,
422
+ // Read here rather than left to the status: `status: 0` reaches
423
+ // `adminErrorMessage` as "nothing is known about this failure", and
424
+ // this seam knows better — it held the response.
425
+ transportFailure: !serverAnswered(res.status),
426
+ code: readErrorCode(body),
427
+ body,
428
+ url,
429
+ method,
430
+ // Shared with `toAdminError` on purpose: a NestJS `ValidationPipe`
431
+ // rejection carries `message` as an array, and reading it here in a
432
+ // second, narrower way is how those constraints went missing.
433
+ detail: readErrorDetail(body)
434
+ });
435
+ }
436
+ function trimTrailingSlashes(url) {
437
+ let end = url.length;
438
+ while (end > 0 && url[end - 1] === "/") end--;
439
+ return url.slice(0, end);
440
+ }
441
+ async function getJson(http, url) {
442
+ const res = await http(url);
443
+ if (res.status < 200 || res.status >= 300) {
444
+ throw await failed(res, "GET", url);
445
+ }
446
+ return await res.json();
447
+ }
448
+ async function postJson(http, url, body) {
449
+ const res = await http(url, {
450
+ method: "POST",
451
+ headers: { "Content-Type": "application/json" },
452
+ body: JSON.stringify(body)
453
+ });
454
+ if (res.status < 200 || res.status >= 300) {
455
+ throw await failed(res, "POST", url);
456
+ }
457
+ return await res.json();
458
+ }
459
+
460
+ // src/client/http/fetch-http-client.ts
461
+ var ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:|^\/\//i;
462
+ function resolveUrl(url, baseUrl) {
463
+ if (!baseUrl || ABSOLUTE_URL.test(url)) return url;
464
+ return `${trimTrailingSlashes(baseUrl)}${url.startsWith("/") ? "" : "/"}${url}`;
465
+ }
466
+ function createFetchHttpClient(options = {}) {
467
+ return async (url, init) => {
468
+ const headers = new Headers(await options.headers?.());
469
+ if (!headers.has("Accept")) headers.set("Accept", "application/json");
470
+ for (const [name, value] of Object.entries(init?.headers ?? {})) {
471
+ headers.set(name, value);
472
+ }
473
+ if (init?.body !== void 0 && !headers.has("Content-Type")) {
474
+ headers.set("Content-Type", "application/json");
475
+ }
476
+ let response;
477
+ try {
478
+ response = await fetch(resolveUrl(url, options.baseUrl), {
479
+ method: init?.method ?? "GET",
480
+ headers,
481
+ body: init?.body
482
+ });
483
+ } catch (err) {
484
+ throw markTransportFailure(err);
485
+ }
486
+ return response;
487
+ };
488
+ }
489
+
163
490
  // src/client/types.ts
164
491
  var NOOP_KV_STORE = {
165
492
  get: () => null,
@@ -201,58 +528,334 @@ function resolveLocalStorage() {
201
528
  }
202
529
  }
203
530
  function defaultHttpClient() {
204
- return (url, init) => fetch(url, init);
531
+ return createFetchHttpClient();
205
532
  }
206
533
 
207
- // src/client/http-json.ts
208
- var HttpJsonError = class extends Error {
209
- constructor(status, code) {
210
- super(code ?? `HTTP ${status}`);
211
- this.status = status;
212
- this.code = code;
213
- this.name = "HttpJsonError";
534
+ // src/client/http/axios-http-client.ts
535
+ function stripped(url, prefixes) {
536
+ for (const prefix of prefixes) {
537
+ const trimmed = trimTrailingSlashes(prefix);
538
+ if (!trimmed || !url.startsWith(trimmed)) continue;
539
+ const rest = url.slice(trimmed.length);
540
+ if (rest === "") return "/";
541
+ if (rest.startsWith("/")) return rest;
542
+ if (rest.startsWith("?") || rest.startsWith("#")) return `/${rest}`;
543
+ }
544
+ return url;
545
+ }
546
+ function headerReader(headers) {
547
+ const record = typeof headers === "object" && headers !== null ? headers : {};
548
+ const lowered = /* @__PURE__ */ new Map();
549
+ for (const [key, value] of Object.entries(record)) lowered.set(key.toLowerCase(), value);
550
+ return (name) => {
551
+ const value = lowered.get(name.toLowerCase());
552
+ return value == null ? null : String(value);
553
+ };
554
+ }
555
+ function bodyIsRaw(response, declared) {
556
+ if (declared !== "auto") return declared === "raw";
557
+ if (response.data === "") return true;
558
+ const config = response.config;
559
+ if (config?.transformResponse === null) return true;
560
+ if (Array.isArray(config?.transformResponse) && config.transformResponse.length === 0) {
561
+ return true;
214
562
  }
215
- status;
216
- code;
217
- };
218
- async function extractCode(res) {
219
- try {
220
- const body = await res.json();
221
- return typeof body?.code === "string" ? body.code : void 0;
222
- } catch {
223
- return void 0;
563
+ const responseType = config?.responseType;
564
+ if (responseType === "json") return false;
565
+ if (typeof responseType === "string" && responseType !== "") return true;
566
+ const transitional = config?.transitional;
567
+ return typeof transitional === "object" && transitional !== null && transitional.forcedJSONParsing === false;
568
+ }
569
+ async function readBody(data) {
570
+ if (typeof data === "string" || data === null || data === void 0) return data;
571
+ if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
572
+ if (ArrayBuffer.isView(data)) return new TextDecoder().decode(data);
573
+ const maybe = data;
574
+ if (typeof maybe.pipe === "function" || typeof maybe.getReader === "function") {
575
+ throw new TypeError(
576
+ "createAxiosHttpClient cannot read a streamed body: responseType 'stream' can be consumed only once, and never synchronously. Use 'arraybuffer' or the default for the instance the platform receives."
577
+ );
224
578
  }
579
+ if (typeof maybe.text === "function") return maybe.text.call(data);
580
+ return data;
225
581
  }
226
- function trimTrailingSlashes(url) {
227
- let end = url.length;
228
- while (end > 0 && url[end - 1] === "/") end--;
229
- return url.slice(0, end);
582
+ function adapt(response, declared) {
583
+ return {
584
+ status: response.status,
585
+ headers: { get: headerReader(response.headers) },
586
+ json: async () => {
587
+ const data = await readBody(response.data);
588
+ if (typeof data !== "string") return data;
589
+ return bodyIsRaw({ ...response, data }, declared) ? JSON.parse(data) : data;
590
+ },
591
+ text: async () => {
592
+ const data = await readBody(response.data);
593
+ return typeof data === "string" ? data : JSON.stringify(data);
594
+ }
595
+ };
230
596
  }
231
- async function getJson(http, url) {
232
- const res = await http(url);
233
- if (res.status < 200 || res.status >= 300) {
234
- throw new HttpJsonError(res.status, await extractCode(res));
597
+ function createAxiosHttpClient(instance, options = {}) {
598
+ const prefixes = typeof options.stripPrefix === "string" ? [options.stripPrefix] : options.stripPrefix ?? [];
599
+ const responseBody = options.responseBody ?? "auto";
600
+ return async (url, init) => {
601
+ try {
602
+ return adapt(
603
+ await instance.request({
604
+ url: stripped(url, prefixes),
605
+ method: (init?.method ?? "GET").toUpperCase(),
606
+ headers: init?.headers,
607
+ data: init?.body
608
+ }),
609
+ responseBody
610
+ );
611
+ } catch (err) {
612
+ const response = err?.response;
613
+ if (response && typeof response.status === "number") {
614
+ return adapt(response, responseBody);
615
+ }
616
+ throw isAxiosNoResponseError(err) ? markTransportFailure(err) : err;
617
+ }
618
+ };
619
+ }
620
+
621
+ // src/client/resources/define-resource.ts
622
+ function defineResource(key, ops, options = {}) {
623
+ return { key, ops, projectScoped: options.projectScoped ?? false };
624
+ }
625
+ function requireProjectKey(def, ctx) {
626
+ if (!def.projectScoped) return;
627
+ if (typeof ctx.projectKey === "string" && ctx.projectKey.trim() !== "") return;
628
+ throw new Error(
629
+ `Resource "${def.key}" is project-scoped, but the context it is bound to names no project. Its requests would go out as \`?projectKey=\`, which the admin API answers for no project at all: the catalogue reads as empty and the first create fails validation instead. Name the project this admin administers \u2014 \`endpoints.projectKey\` for createSuperAdminApp(), \`context.projectKey\` for createResourceRegistry().`
630
+ );
631
+ }
632
+ function bindResource(def, http, ctx) {
633
+ const readContext = typeof ctx === "function" ? ctx : () => ctx;
634
+ if (def.projectScoped) requireProjectKey(def, readContext());
635
+ const bound = {};
636
+ for (const [name, op] of Object.entries(def.ops)) {
637
+ bound[name] = async (...args) => {
638
+ const context = readContext();
639
+ requireProjectKey(def, context);
640
+ return op(http, context, ...args);
641
+ };
235
642
  }
236
- return await res.json();
643
+ return bound;
237
644
  }
238
- async function postJson(http, url, body) {
239
- const res = await http(url, {
240
- method: "POST",
241
- headers: { "Content-Type": "application/json" },
242
- body: JSON.stringify(body)
645
+
646
+ // src/client/resources/resource-request.ts
647
+ async function requestJson(http, url, init = {}) {
648
+ const method = init.method ?? "GET";
649
+ const response = await http(url, {
650
+ method,
651
+ headers: { "content-type": "application/json", ...init.headers },
652
+ body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
243
653
  });
244
- if (res.status < 200 || res.status >= 300) {
245
- throw new HttpJsonError(res.status, await extractCode(res));
654
+ requireServerAnswer(
655
+ response.status,
656
+ method,
657
+ url,
658
+ (diagnostic) => new AdminError({ status: response.status, url, method, message: diagnostic })
659
+ );
660
+ if (response.status === 204) return null;
661
+ const body = await response.json().catch(() => null);
662
+ if (response.status >= 400) {
663
+ throw new AdminError({
664
+ status: response.status,
665
+ code: readErrorCode(body),
666
+ body,
667
+ // The third place this had to be read, and the second time it was
668
+ // read a narrower way: without `detail` a page migrating onto these
669
+ // resources loses "Plan already exists" and shows the generic
670
+ // status sentence instead. One decision, one function.
671
+ detail: readErrorDetail(body),
672
+ url,
673
+ method
674
+ });
246
675
  }
247
- return await res.json();
676
+ return body;
677
+ }
678
+ async function requestJsonBody(http, url, what, init = {}) {
679
+ const body = await requestJson(http, url, init);
680
+ if (body === null || body === void 0) {
681
+ throw new AdminError({
682
+ status: 0,
683
+ url,
684
+ method: init.method ?? "GET",
685
+ // `what` is a diagnostic — "Create returned no body" — so it goes
686
+ // on `message`, not `detail`. As `detail` it out-ranked the
687
+ // localized wording and the operator read an English internal
688
+ // string instead of being told the change may already have landed.
689
+ message: what,
690
+ emptyResponse: true
691
+ });
692
+ }
693
+ return body;
694
+ }
695
+
696
+ // src/client/resources/list-resource.ts
697
+ var LIST_FIRST_PAGE = 1;
698
+ var LIST_PAGE_SIZE_DEFAULT = 50;
699
+ var LIST_PAGE_SIZE_MAX = 200;
700
+ var LIST_PAGINATION_PARAMS = ["page", "pageSize"];
701
+ function isSentInQuery(value) {
702
+ return value !== void 0 && value !== null && value !== "";
703
+ }
704
+ function appendFilter(params, filter) {
705
+ for (const [key, value] of Object.entries(filter ?? {})) {
706
+ if (!isSentInQuery(value)) continue;
707
+ params.set(key, String(value));
708
+ }
709
+ }
710
+ function filterQueryString(filter) {
711
+ const params = new URLSearchParams();
712
+ appendFilter(params, filter);
713
+ const query = params.toString();
714
+ return query ? `?${query}` : "";
715
+ }
716
+ function listUrl(endpoint, query = {}) {
717
+ const params = new URLSearchParams();
718
+ params.set("page", String(query.page ?? LIST_FIRST_PAGE));
719
+ params.set("pageSize", String(query.pageSize ?? LIST_PAGE_SIZE_DEFAULT));
720
+ appendFilter(params, query.filter);
721
+ return `${endpoint}${endpoint.includes("?") ? "&" : "?"}${params.toString()}`;
722
+ }
723
+ function readListPage(raw) {
724
+ if (Array.isArray(raw)) return { items: raw, total: raw.length };
725
+ if (raw === null || typeof raw !== "object") return { items: [], total: 0 };
726
+ const body = raw;
727
+ const page = {
728
+ items: Array.isArray(body.items) ? body.items : []
729
+ };
730
+ if (typeof body.total === "number") page.total = body.total;
731
+ if (typeof body.page === "number") page.page = body.page;
732
+ if (typeof body.pageSize === "number") page.pageSize = body.pageSize;
733
+ return page;
734
+ }
735
+ function clampListPage(page) {
736
+ return Math.max(LIST_FIRST_PAGE, Math.floor(page));
737
+ }
738
+ function clampListPageSize(size) {
739
+ return Math.min(LIST_PAGE_SIZE_MAX, Math.max(1, Math.floor(size)));
740
+ }
741
+ function defineListOp(endpoint) {
742
+ return async (http, ctx, query) => readListPage(await requestJson(http, listUrl(endpoint(ctx), query)));
248
743
  }
249
744
 
745
+ // src/client/resources/plans.resource.ts
746
+ function plansUrl(ctx) {
747
+ return `${ctx.apiBase}/catalog/plans`;
748
+ }
749
+ function versionsUrl(ctx) {
750
+ return `${ctx.apiBase}/catalog/plan-versions`;
751
+ }
752
+ function scoped(url, ctx) {
753
+ return `${url}?projectKey=${encodeURIComponent(ctx.projectKey)}`;
754
+ }
755
+ var plansResource = defineResource(
756
+ "plans",
757
+ {
758
+ list: async (http, ctx) => await requestJson(http, scoped(plansUrl(ctx), ctx)) ?? [],
759
+ tenantCounts: async (http, ctx) => await requestJson(
760
+ http,
761
+ scoped(`${plansUrl(ctx)}/tenant-counts`, ctx)
762
+ ) ?? {},
763
+ /**
764
+ * The project comes from the context, not from the caller.
765
+ *
766
+ * The endpoint takes it in the body, and the composable this replaces
767
+ * made that the caller's job — so a page had to keep hold of the
768
+ * project it was administering just to create in it, and `list()` and
769
+ * `create()` read two different sources for one value. A stale one
770
+ * creates a plan outside the project the list is showing.
771
+ */
772
+ create: (http, ctx, data) => requestJsonBody(http, plansUrl(ctx), "Create returned no body", {
773
+ method: "POST",
774
+ body: { ...data, projectKey: ctx.projectKey }
775
+ }),
776
+ update: (http, ctx, planId, data) => requestJsonBody(
777
+ http,
778
+ `${plansUrl(ctx)}/${planId}`,
779
+ "Update returned no body",
780
+ {
781
+ method: "PATCH",
782
+ body: data
783
+ }
784
+ ),
785
+ /** Marks the plan deleted. Fails while it still has versions. */
786
+ softDelete: async (http, ctx, planId) => {
787
+ await requestJson(http, `${plansUrl(ctx)}/${planId}`, { method: "DELETE" });
788
+ },
789
+ /** Removes the plan for good. 422 `PLAN_HAS_VERSIONS` when it cannot. */
790
+ hardDelete: async (http, ctx, planId) => {
791
+ await requestJson(http, `${plansUrl(ctx)}/${planId}/purge`, { method: "DELETE" });
792
+ }
793
+ },
794
+ { projectScoped: true }
795
+ );
796
+ var planVersionsResource = defineResource("planVersions", {
797
+ listForPlan: async (http, ctx, planId) => await requestJson(http, `${plansUrl(ctx)}/${planId}/versions`) ?? [],
798
+ createDraft: (http, ctx, planId, data) => requestJsonBody(
799
+ http,
800
+ `${plansUrl(ctx)}/${planId}/versions`,
801
+ "CreateDraft returned no body",
802
+ { method: "POST", body: data }
803
+ ),
804
+ updateDraft: (http, ctx, versionId, data) => requestJsonBody(
805
+ http,
806
+ `${versionsUrl(ctx)}/${versionId}`,
807
+ "UpdateDraft returned no body",
808
+ { method: "PATCH", body: data }
809
+ ),
810
+ publish: (http, ctx, versionId, options = {}) => requestJsonBody(
811
+ http,
812
+ `${versionsUrl(ctx)}/${versionId}/publish`,
813
+ "Publish returned no body",
814
+ { method: "POST", body: options }
815
+ ),
816
+ discardDraft: async (http, ctx, versionId) => {
817
+ await requestJson(http, `${versionsUrl(ctx)}/${versionId}`, { method: "DELETE" });
818
+ },
819
+ /**
820
+ * Ends a live version on `endsAt`, with no successor. Idempotent — a
821
+ * second call with another date overwrites the first.
822
+ *
823
+ * `endsAt` is the date itself, not a payload: the endpoint's body is
824
+ * `{ endsAt }` and wrapping it is this operation's job, not the caller's.
825
+ */
826
+ terminate: (http, ctx, versionId, endsAt) => requestJsonBody(
827
+ http,
828
+ `${versionsUrl(ctx)}/${versionId}/terminate`,
829
+ "Terminate returned no body",
830
+ { method: "POST", body: { endsAt } }
831
+ )
832
+ });
833
+
834
+ // src/client/resources/tenants.resource.ts
835
+ var tenantsResource = defineResource("tenants", {
836
+ list: defineListOp((ctx) => `${ctx.apiBase}/tenants`)
837
+ });
838
+
839
+ // src/client/resources/audit.resource.ts
840
+ var auditResource = defineResource("audit", {
841
+ list: defineListOp((ctx) => `${ctx.apiBase}/audit`)
842
+ });
843
+
844
+ // src/client/resources/index.ts
845
+ var platformResources = {
846
+ plans: plansResource,
847
+ planVersions: planVersionsResource,
848
+ tenants: tenantsResource,
849
+ audit: auditResource
850
+ };
851
+
250
852
  // src/client/boot-loader.ts
251
853
  var BootLoadError = class extends Error {
252
854
  constructor(status, message) {
253
855
  super(message);
254
856
  this.status = status;
255
857
  this.name = "BootLoadError";
858
+ markPlatformError(this);
256
859
  }
257
860
  status;
258
861
  };
@@ -270,19 +873,27 @@ var BootLoader = class {
270
873
  }
271
874
  async load() {
272
875
  const res = await this.http(this.endpoint);
876
+ requireServerAnswer(
877
+ res.status,
878
+ "GET",
879
+ this.endpoint,
880
+ (diagnostic) => new BootLoadError(res.status, diagnostic)
881
+ );
273
882
  if (res.status !== 200) {
274
- throw new BootLoadError(res.status, `Boot-Endpunkt antwortete HTTP ${res.status}`);
883
+ throw new BootLoadError(res.status, `Boot endpoint responded with HTTP ${res.status}`);
275
884
  }
276
885
  return await res.json();
277
886
  }
278
887
  };
279
888
 
280
889
  // src/client/manifest-loader.ts
890
+ var NOT_MODIFIED = 304;
281
891
  var ManifestLoadError = class extends Error {
282
892
  constructor(status, message) {
283
893
  super(message);
284
894
  this.status = status;
285
895
  this.name = "ManifestLoadError";
896
+ markPlatformError(this);
286
897
  }
287
898
  status;
288
899
  };
@@ -310,24 +921,48 @@ var ManifestLoader = class {
310
921
  /**
311
922
  * Loads the current manifest. On a cache hit (304) the cached
312
923
  * body is returned — otherwise the fresh server body.
924
+ *
925
+ * A 304 with no usable cached body is not a failure to report but one to
926
+ * undo: the request only carried `If-None-Match` because this loader put
927
+ * the ETag there, and the cache it validated against is this loader's own.
928
+ * So it drops the pair and asks once more without the header, which is the
929
+ * same recovery a person would have had to trigger by hand — except that
930
+ * the person reading an admin screen has neither this loader nor a
931
+ * console. Exactly one retry: a 304 to a request that asked nothing
932
+ * conditional is a server fault, and no repetition fixes it.
313
933
  */
314
934
  async load() {
315
935
  const cachedEtag = this.storage.get(this.etagKey);
936
+ const res = await this.request(cachedEtag);
937
+ if (res.status !== NOT_MODIFIED) return this.acceptFresh(res);
938
+ const cached = this.readCachedBody();
939
+ if (cached) return cached.body;
940
+ if (cachedEtag) {
941
+ this.clearCache();
942
+ const fresh = await this.request(null);
943
+ if (fresh.status !== NOT_MODIFIED) return this.acceptFresh(fresh);
944
+ }
945
+ throw new ManifestLoadError(
946
+ NOT_MODIFIED,
947
+ "Manifest endpoint answered HTTP 304 to a request without If-None-Match"
948
+ );
949
+ }
950
+ /** Sends the manifest GET, conditional when an ETag is supplied. */
951
+ request(etag) {
316
952
  const headers = {};
317
953
  const token = this.getAuthToken?.();
318
954
  if (token) headers.Authorization = `Bearer ${token}`;
319
- if (cachedEtag) headers["If-None-Match"] = cachedEtag;
320
- const res = await this.http(this.endpoint, { method: "GET", headers });
321
- if (res.status === 304) {
322
- const cachedBody = this.readCachedBody();
323
- if (!cachedBody) {
324
- throw new ManifestLoadError(
325
- 304,
326
- "Server returned 304 but the cache body is missing \u2014 call clearCache() and reload"
327
- );
328
- }
329
- return cachedBody.body;
330
- }
955
+ if (etag) headers["If-None-Match"] = etag;
956
+ return this.http(this.endpoint, { method: "GET", headers });
957
+ }
958
+ /** Takes a non-304 response as the current manifest and caches it. */
959
+ async acceptFresh(res) {
960
+ requireServerAnswer(
961
+ res.status,
962
+ "GET",
963
+ this.endpoint,
964
+ (diagnostic) => new ManifestLoadError(res.status, diagnostic)
965
+ );
331
966
  if (res.status !== 200) {
332
967
  throw new ManifestLoadError(
333
968
  res.status,
@@ -662,7 +1297,7 @@ var ActionRegistry = class {
662
1297
  // src/client/batch-column-fetcher.ts
663
1298
  var BatchColumnDriftError = class extends Error {
664
1299
  constructor(column, reason) {
665
- super(`Spalte "${column.key}": ${reason}`);
1300
+ super(`Column "${column.key}": ${reason}`);
666
1301
  this.column = column;
667
1302
  this.name = "BatchColumnDriftError";
668
1303
  }
@@ -705,7 +1340,7 @@ var BatchColumnFetcher = class {
705
1340
  const res = await this.http(url, { method: "GET", headers });
706
1341
  if (res.status !== 200) {
707
1342
  throw new Error(
708
- `Spalte "${column.key}" \u2014 Endpoint ${column.endpoint} antwortete HTTP ${res.status}`
1343
+ `Column "${column.key}" \u2014 endpoint ${column.endpoint} responded with HTTP ${res.status}`
709
1344
  );
710
1345
  }
711
1346
  const body = await res.json();
@@ -781,10 +1416,10 @@ function createAdminResourceClient(options) {
781
1416
  tenantsEndpoint,
782
1417
  loadTenantDetail: (slug) => getJson2(`${tenantsEndpoint}/${encodeURIComponent(slug)}`),
783
1418
  loadUsers: (filter) => getJson2(
784
- `${base}/users${queryString({ q: filter.q, tenant: filter.tenant })}`
1419
+ `${base}/users${filterQueryString({ q: filter.q, tenant: filter.tenant })}`
785
1420
  ),
786
1421
  loadAudit: (filter) => getJson2(
787
- `${base}/audit${queryString({
1422
+ `${base}/audit${filterQueryString({
788
1423
  actor: filter.actor,
789
1424
  action: filter.action,
790
1425
  entity: filter.entity,
@@ -794,7 +1429,7 @@ function createAdminResourceClient(options) {
794
1429
  ),
795
1430
  loadSubscriptions: () => getJson2(`${base}/subscriptions`),
796
1431
  loadPromos: (filter) => getJson2(
797
- `${base}/promo-codes${queryString({
1432
+ `${base}/promo-codes${filterQueryString({
798
1433
  search: filter.search,
799
1434
  status: filter.status
800
1435
  })}`
@@ -819,21 +1454,6 @@ function assertSuccess(status, method, url) {
819
1454
  throw new Error(`${method} ${url} \u2192 HTTP ${status}`);
820
1455
  }
821
1456
  }
822
- function queryString(params) {
823
- const search = new URLSearchParams();
824
- for (const [key, value2] of Object.entries(params)) {
825
- if (value2 === void 0 || value2 === null || value2 === "") continue;
826
- search.set(key, String(value2));
827
- }
828
- const value = search.toString();
829
- return value ? `?${value}` : "";
830
- }
831
-
832
- // src/client/i18n/format.ts
833
- var import_types4 = require("@saasicat/types");
834
- function formatMessage(template, params) {
835
- return (0, import_types4.formatErrorMessage)(template, params);
836
- }
837
1457
 
838
1458
  // src/client/i18n/currency.ts
839
1459
  var DEFAULT_CURRENCY = "EUR";
@@ -1532,9 +2152,6 @@ var discoveryMessages = defineMessages(
1532
2152
  replacedBy: "ersetzt durch {key}",
1533
2153
  replaces: "ersetzt: {keys}",
1534
2154
  reapproveEmphasis: "erneut freigeben",
1535
- errorDiscoveryHttp: "Discovery-Endpoint antwortete mit HTTP {status}",
1536
- errorRescanHttp: "Discovery-Rescan antwortete mit HTTP {status}",
1537
- errorCatalogHttp: "Catalog-Entries-API antwortete mit HTTP {status}",
1538
2155
  capList: {
1539
2156
  orphanFeature: "Feature im Katalog ohne implementierende Capability \u2014 blockiert im blocking-Strict-Mode das Plan-Publish.",
1540
2157
  removedFromCode: "aus Code entfernt"
@@ -1636,9 +2253,6 @@ var discoveryMessages = defineMessages(
1636
2253
  replacedBy: "replaced by {key}",
1637
2254
  replaces: "replaces: {keys}",
1638
2255
  reapproveEmphasis: "approve again",
1639
- errorDiscoveryHttp: "Discovery endpoint responded with HTTP {status}",
1640
- errorRescanHttp: "Discovery rescan responded with HTTP {status}",
1641
- errorCatalogHttp: "Catalog entries API responded with HTTP {status}",
1642
2256
  capList: {
1643
2257
  orphanFeature: "Feature in the catalog without an implementing capability \u2014 blocks plan publishing in blocking strict mode.",
1644
2258
  removedFromCode: "removed from code"
@@ -1843,6 +2457,36 @@ var emailMessages = defineMessages(
1843
2457
  }
1844
2458
  );
1845
2459
 
2460
+ // src/client/i18n/messages/errors.ts
2461
+ var errorsMessages = defineMessages(
2462
+ {
2463
+ network: "Die Anfrage konnte nicht abgeschlossen werden. Bitte Verbindung pr\xFCfen.",
2464
+ emptyResponse: "Der Server hat nichts zur\xFCckgegeben. Bitte pr\xFCfen, ob die \xC4nderung angekommen ist.",
2465
+ unauthorized: "Die Sitzung ist abgelaufen. Bitte erneut anmelden.",
2466
+ forbidden: "F\xFCr diese Aktion fehlt die Berechtigung.",
2467
+ notFound: "Der angeforderte Eintrag existiert nicht mehr.",
2468
+ conflict: "Der Eintrag wurde zwischenzeitlich ge\xE4ndert. Bitte neu laden.",
2469
+ validation: "Die Eingaben wurden nicht akzeptiert.",
2470
+ rateLimited: "Zu viele Anfragen. Bitte kurz warten und erneut versuchen.",
2471
+ server: "Der Server konnte die Anfrage nicht verarbeiten.",
2472
+ httpStatus: "Die Anfrage ist fehlgeschlagen (HTTP {status}).",
2473
+ unexpected: "Es ist ein unerwarteter Fehler aufgetreten. Bitte erneut versuchen."
2474
+ },
2475
+ {
2476
+ network: "The request could not be completed. Please check your connection.",
2477
+ emptyResponse: "The server returned nothing. Please check whether the change was applied.",
2478
+ unauthorized: "Your session has expired. Please sign in again.",
2479
+ forbidden: "You do not have permission for this action.",
2480
+ notFound: "The requested entry no longer exists.",
2481
+ conflict: "The entry changed in the meantime. Please reload.",
2482
+ validation: "The input was not accepted.",
2483
+ rateLimited: "Too many requests. Please wait a moment and try again.",
2484
+ server: "The server could not process the request.",
2485
+ httpStatus: "The request failed (HTTP {status}).",
2486
+ unexpected: "Something went wrong. Please try again."
2487
+ }
2488
+ );
2489
+
1846
2490
  // src/client/i18n/messages/marketing.ts
1847
2491
  var marketingMessages = defineMessages(
1848
2492
  {
@@ -1967,9 +2611,6 @@ var marketingMessages = defineMessages(
1967
2611
  colorLabel: "Farbe",
1968
2612
  delete: "Aktion l\xF6schen",
1969
2613
  deleteConfirm: "Aktion \u201E{label}\u201C wirklich l\xF6schen?"
1970
- },
1971
- errors: {
1972
- projectionsApi: "MarketingProjections-API antwortete mit HTTP {status}"
1973
2614
  }
1974
2615
  },
1975
2616
  {
@@ -2094,9 +2735,6 @@ var marketingMessages = defineMessages(
2094
2735
  colorLabel: "Color",
2095
2736
  delete: "Delete promotion",
2096
2737
  deleteConfirm: 'Really delete promotion "{label}"?'
2097
- },
2098
- errors: {
2099
- projectionsApi: "MarketingProjections API responded with HTTP {status}"
2100
2738
  }
2101
2739
  }
2102
2740
  );
@@ -3290,7 +3928,6 @@ var promosMessages = defineMessages(
3290
3928
  {
3291
3929
  title: "Promo-Codes",
3292
3930
  createAction: "Promo-Code anlegen",
3293
- apiErrorHttpStatus: "Promotions-API antwortete mit HTTP {status}",
3294
3931
  list: {
3295
3932
  searchLabel: "Suche (Code)",
3296
3933
  tileScheduled: "Geplant",
@@ -3388,7 +4025,6 @@ var promosMessages = defineMessages(
3388
4025
  {
3389
4026
  title: "Promo codes",
3390
4027
  createAction: "Create promo code",
3391
- apiErrorHttpStatus: "Promotions API responded with HTTP {status}",
3392
4028
  list: {
3393
4029
  searchLabel: "Search (code)",
3394
4030
  tileScheduled: "Scheduled",
@@ -3496,7 +4132,13 @@ var shellMessages = defineMessages(
3496
4132
  subtitle: "SuperAdmin \xB7 Plattform-Verwaltung",
3497
4133
  roleBadge: "SUPER ADMIN",
3498
4134
  logout: "Abmelden",
3499
- language: "Sprache"
4135
+ language: "Sprache",
4136
+ theme: {
4137
+ label: "Darstellung",
4138
+ light: "Hell",
4139
+ dark: "Dunkel",
4140
+ system: "System"
4141
+ }
3500
4142
  },
3501
4143
  drawer: {
3502
4144
  docs: "Doku \xF6ffnen"
@@ -3565,7 +4207,13 @@ var shellMessages = defineMessages(
3565
4207
  subtitle: "SuperAdmin \xB7 Platform administration",
3566
4208
  roleBadge: "SUPER ADMIN",
3567
4209
  logout: "Sign out",
3568
- language: "Language"
4210
+ language: "Language",
4211
+ theme: {
4212
+ label: "Appearance",
4213
+ light: "Light",
4214
+ dark: "Dark",
4215
+ system: "System"
4216
+ }
3569
4217
  },
3570
4218
  drawer: {
3571
4219
  docs: "Open documentation"
@@ -3809,6 +4457,7 @@ var usersMessages = defineMessages(
3809
4457
  function catalogFor(locale) {
3810
4458
  return {
3811
4459
  common: commonMessages[locale],
4460
+ errors: errorsMessages[locale],
3812
4461
  nav: navMessages[locale],
3813
4462
  shell: shellMessages[locale],
3814
4463
  dashboard: dashboardMessages[locale],
@@ -4063,6 +4712,11 @@ var SUPER_ADMIN_NOTIFY_KEY = /* @__PURE__ */ Symbol.for(
4063
4712
  "@saasicat/ui-vue/SUPER_ADMIN_NOTIFY"
4064
4713
  );
4065
4714
 
4715
+ // src/vue/ui-confirm.ts
4716
+ var SUPER_ADMIN_CONFIRM_KEY = /* @__PURE__ */ Symbol.for(
4717
+ "@saasicat/ui-vue/SUPER_ADMIN_CONFIRM"
4718
+ );
4719
+
4066
4720
  // src/vue/use-super-admin-context.ts
4067
4721
  var import_vue = require("vue");
4068
4722
  function useSuperAdminExtensions() {
@@ -4153,10 +4807,10 @@ function useSaMessages(namespace) {
4153
4807
 
4154
4808
  // src/vue/use-sa-theme.ts
4155
4809
  var import_vue3 = require("vue");
4156
- var SCHEMES = ["light", "dark", "system"];
4157
- var STORAGE_KEY = "saasicat.theme.scheme";
4810
+ var SA_COLOR_SCHEMES = ["light", "dark", "system"];
4811
+ var SA_THEME_STORAGE_KEY = "saasicat.theme.scheme";
4158
4812
  function isScheme(value) {
4159
- return typeof value === "string" && SCHEMES.includes(value);
4813
+ return typeof value === "string" && SA_COLOR_SCHEMES.includes(value);
4160
4814
  }
4161
4815
  function systemScheme() {
4162
4816
  const value = (0, import_vue3.ref)("light");
@@ -4177,13 +4831,19 @@ function createSaTheme(options = {}) {
4177
4831
  const persist = options.persist !== false;
4178
4832
  const provided = options.scheme;
4179
4833
  const appOwnsIt = (0, import_vue3.isRef)(provided);
4180
- const stored = persist && !appOwnsIt ? storage.get(STORAGE_KEY) : null;
4834
+ const key = `${options.storageKeyPrefix ?? ""}${SA_THEME_STORAGE_KEY}`;
4835
+ const stored = persist && !appOwnsIt ? storage.get(key) : null;
4181
4836
  const scheme = (0, import_vue3.isRef)(provided) ? provided : (0, import_vue3.ref)(isScheme(stored) ? stored : provided ?? "system");
4182
4837
  const { value: system, unsubscribe } = systemScheme();
4183
- const stopPersisting = persist && !appOwnsIt ? (0, import_vue3.watch)(scheme, (next) => storage.set(STORAGE_KEY, next)) : null;
4838
+ const stopPersisting = persist && !appOwnsIt ? (0, import_vue3.watch)(scheme, (next) => storage.set(key, next)) : null;
4184
4839
  return {
4185
4840
  scheme,
4186
4841
  resolved: (0, import_vue3.computed)(() => scheme.value === "system" ? system.value : scheme.value),
4842
+ // A readonly ref (typically a `computed` derived from a profile
4843
+ // setting) silently swallows writes, so the switcher would be a dead
4844
+ // control. TypeScript ignores `readonly` when checking assignability,
4845
+ // so the guard has to be a runtime one — same as the locale next door.
4846
+ switcherEnabled: (options.switcher ?? true) && !(0, import_vue3.isReadonly)(scheme),
4187
4847
  dispose: () => {
4188
4848
  unsubscribe();
4189
4849
  stopPersisting?.();
@@ -4295,22 +4955,17 @@ var import_vue5 = require("vue");
4295
4955
  function useApiList(options) {
4296
4956
  const http = options.http ?? defaultHttpClient();
4297
4957
  const items = (0, import_vue5.ref)([]);
4298
- const page = (0, import_vue5.ref)(1);
4299
- const pageSize = (0, import_vue5.ref)(50);
4958
+ const page = (0, import_vue5.ref)(LIST_FIRST_PAGE);
4959
+ const pageSize = (0, import_vue5.ref)(LIST_PAGE_SIZE_DEFAULT);
4300
4960
  const total = (0, import_vue5.ref)(0);
4301
4961
  const loading = (0, import_vue5.ref)(false);
4302
4962
  const error = (0, import_vue5.ref)(null);
4303
4963
  function buildUrl() {
4304
- const params = new URLSearchParams();
4305
- params.set("page", String(page.value));
4306
- params.set("pageSize", String(pageSize.value));
4307
- const f = options.filter?.value ?? {};
4308
- for (const [k, v] of Object.entries(f)) {
4309
- if (v === void 0 || v === null || v === "") continue;
4310
- params.set(k, String(v));
4311
- }
4312
- const sep = options.endpoint.includes("?") ? "&" : "?";
4313
- return `${options.endpoint}${sep}${params.toString()}`;
4964
+ return listUrl(options.endpoint, {
4965
+ page: page.value,
4966
+ pageSize: pageSize.value,
4967
+ filter: options.filter?.value
4968
+ });
4314
4969
  }
4315
4970
  async function load() {
4316
4971
  loading.value = true;
@@ -4323,20 +4978,11 @@ function useApiList(options) {
4323
4978
  if (res.status !== 200) {
4324
4979
  throw new Error(`Endpoint ${options.endpoint} \u2192 HTTP ${res.status}`);
4325
4980
  }
4326
- const raw = await res.json();
4327
- if (Array.isArray(raw)) {
4328
- items.value = raw;
4329
- total.value = raw.length;
4330
- } else if (raw !== null && typeof raw === "object") {
4331
- const body = raw;
4332
- items.value = body.items ?? [];
4333
- if (typeof body.page === "number") page.value = body.page;
4334
- if (typeof body.pageSize === "number") pageSize.value = body.pageSize;
4335
- if (typeof body.total === "number") total.value = body.total;
4336
- } else {
4337
- items.value = [];
4338
- total.value = 0;
4339
- }
4981
+ const body = readListPage(await res.json());
4982
+ items.value = body.items;
4983
+ if (typeof body.page === "number") page.value = body.page;
4984
+ if (typeof body.pageSize === "number") pageSize.value = body.pageSize;
4985
+ if (typeof body.total === "number") total.value = body.total;
4340
4986
  } catch (err) {
4341
4987
  error.value = err instanceof Error ? err : new Error(String(err));
4342
4988
  items.value = [];
@@ -4346,19 +4992,19 @@ function useApiList(options) {
4346
4992
  }
4347
4993
  }
4348
4994
  async function goToPage(p) {
4349
- page.value = Math.max(1, Math.floor(p));
4995
+ page.value = clampListPage(p);
4350
4996
  await load();
4351
4997
  }
4352
4998
  async function setPageSize(size) {
4353
- pageSize.value = Math.min(200, Math.max(1, Math.floor(size)));
4354
- page.value = 1;
4999
+ pageSize.value = clampListPageSize(size);
5000
+ page.value = LIST_FIRST_PAGE;
4355
5001
  await load();
4356
5002
  }
4357
5003
  if (options.filter) {
4358
5004
  (0, import_vue5.watch)(
4359
5005
  options.filter,
4360
5006
  () => {
4361
- page.value = 1;
5007
+ page.value = LIST_FIRST_PAGE;
4362
5008
  void load();
4363
5009
  },
4364
5010
  { deep: true }
@@ -4404,15 +5050,253 @@ function usePagination(rows, initialRowsPerPage = DEFAULT_ROWS_PER_PAGE) {
4404
5050
  return { page, rowsPerPage, total, pagedRows };
4405
5051
  }
4406
5052
 
4407
- // src/vue/use-tenants.ts
5053
+ // src/vue/use-async-action.ts
4408
5054
  var import_vue7 = require("vue");
5055
+ function report(announce) {
5056
+ try {
5057
+ announce();
5058
+ } catch (err) {
5059
+ queueMicrotask(() => {
5060
+ throw err;
5061
+ });
5062
+ }
5063
+ }
5064
+ function useAsyncAction(fn, options = {}) {
5065
+ const pending = (0, import_vue7.ref)(false);
5066
+ const error = (0, import_vue7.ref)(null);
5067
+ const notify = options.notify ?? (0, import_vue7.inject)(SUPER_ADMIN_NOTIFY_KEY, void 0);
5068
+ const messages = useSaMessages("errors");
5069
+ const notifyOn = options.notifyOn ?? "error";
5070
+ let inFlight = 0;
5071
+ let generation = 0;
5072
+ async function run(...args) {
5073
+ const mine = ++generation;
5074
+ inFlight++;
5075
+ pending.value = true;
5076
+ error.value = null;
5077
+ try {
5078
+ const result = await fn(...args);
5079
+ await options.onSuccess?.(result);
5080
+ if (notifyOn === "both") {
5081
+ report(() => {
5082
+ const message = typeof options.successMessage === "function" ? options.successMessage() : options.successMessage;
5083
+ if (message) notify?.("positive", message);
5084
+ });
5085
+ }
5086
+ return { ok: true, value: result };
5087
+ } catch (err) {
5088
+ const adminError = toAdminError(err);
5089
+ if (mine === generation) error.value = adminError;
5090
+ if (notifyOn !== "none") {
5091
+ report(
5092
+ () => notify?.(
5093
+ "negative",
5094
+ options.errorMessage?.(adminError) ?? adminErrorMessage(adminError, messages.value)
5095
+ )
5096
+ );
5097
+ }
5098
+ return { ok: false, error: adminError };
5099
+ } finally {
5100
+ if (--inFlight === 0) pending.value = false;
5101
+ }
5102
+ }
5103
+ return {
5104
+ run,
5105
+ pending,
5106
+ error,
5107
+ reset: () => {
5108
+ error.value = null;
5109
+ }
5110
+ };
5111
+ }
5112
+
5113
+ // src/vue/use-async-data.ts
5114
+ var import_vue8 = require("vue");
5115
+ function useAsyncData(fn, options) {
5116
+ const data = (0, import_vue8.ref)(options.initial);
5117
+ const pending = (0, import_vue8.ref)(false);
5118
+ const error = (0, import_vue8.ref)(null);
5119
+ let generation = 0;
5120
+ async function reload() {
5121
+ const mine = ++generation;
5122
+ pending.value = true;
5123
+ error.value = null;
5124
+ try {
5125
+ const result = await fn();
5126
+ if (mine !== generation) return;
5127
+ data.value = result;
5128
+ } catch (err) {
5129
+ if (mine !== generation) return;
5130
+ error.value = toAdminError(err);
5131
+ data.value = options.initial;
5132
+ } finally {
5133
+ if (mine === generation) pending.value = false;
5134
+ }
5135
+ }
5136
+ if (options.watch?.length) {
5137
+ (0, import_vue8.watch)(options.watch, () => void reload());
5138
+ }
5139
+ if (options.immediate !== false) {
5140
+ void Promise.resolve().then(() => reload());
5141
+ }
5142
+ return { data, pending, error, reload };
5143
+ }
5144
+
5145
+ // src/vue/resource-registry.ts
5146
+ var import_vue9 = require("vue");
5147
+ function boundWithOverride(def, http, readContext, override) {
5148
+ const context = override?.context ? () => ({ ...readContext(), ...override.context }) : readContext;
5149
+ const platform = bindResource(def, override?.http ?? http, context);
5150
+ if (!override?.ops) return platform;
5151
+ const result = { ...platform };
5152
+ for (const [name, wrap] of Object.entries(override.ops)) {
5153
+ if (typeof wrap !== "function") continue;
5154
+ if (!Object.hasOwn(platform, name)) {
5155
+ throw new Error(
5156
+ `createResourceRegistry: resource "${def.key}" has no operation "${name}" to override. It offers: ${Object.keys(platform).join(", ")}.`
5157
+ );
5158
+ }
5159
+ const next = platform[name];
5160
+ result[name] = (...args) => wrap(next, ...args);
5161
+ }
5162
+ return result;
5163
+ }
5164
+ function createResourceRegistry(options) {
5165
+ if (!options?.http) {
5166
+ throw new Error(
5167
+ "createResourceRegistry: `http` is required. Pass the HttpClient your app already uses \u2014 createAxiosHttpClient(instance) or createFetchHttpClient({ headers }) \u2014 so every platform request carries its auth."
5168
+ );
5169
+ }
5170
+ const readContext = typeof options.context === "function" ? options.context : () => options.context;
5171
+ const unknown = Object.keys(options.overrides ?? {}).filter(
5172
+ (key) => !Object.hasOwn(options.resources, key)
5173
+ );
5174
+ if (unknown.length > 0) {
5175
+ throw new Error(
5176
+ `createResourceRegistry: no resource named ${unknown.map((k) => `"${k}"`).join(", ")} to override. The registry holds: ${Object.keys(options.resources).join(", ")}.`
5177
+ );
5178
+ }
5179
+ const bound = /* @__PURE__ */ new Map();
5180
+ for (const [key, def] of Object.entries(options.resources)) {
5181
+ bound.set(key, boundWithOverride(def, options.http, readContext, options.overrides?.[key]));
5182
+ }
5183
+ return {
5184
+ get(key) {
5185
+ const ops = bound.get(key);
5186
+ if (!ops) {
5187
+ throw new Error(
5188
+ `useResource("${String(key)}"): no such resource. The registry offers: ${[...bound.keys()].join(", ")}.`
5189
+ );
5190
+ }
5191
+ return ops;
5192
+ },
5193
+ keys: () => [...bound.keys()]
5194
+ };
5195
+ }
5196
+ var SUPER_ADMIN_RESOURCES_KEY = /* @__PURE__ */ Symbol.for(
5197
+ "@saasicat/ui-vue/SUPER_ADMIN_RESOURCES"
5198
+ );
5199
+ function useResource(key) {
5200
+ const registry = (0, import_vue9.inject)(SUPER_ADMIN_RESOURCES_KEY, null);
5201
+ if (!registry) {
5202
+ throw new Error(
5203
+ `useResource("${key}"): no resource registry in scope. Either the component was not mounted inside createSuperAdminApp(), or the app did not pass an \`http\` client \u2014 the registry is not installed without one, because a bare fetch would send every request without your Authorization header and fail silently.`
5204
+ );
5205
+ }
5206
+ return registry.get(key);
5207
+ }
5208
+
5209
+ // src/vue/use-resource-list.ts
5210
+ var import_vue10 = require("vue");
5211
+ var DEFAULT_LIST_OP = "list";
5212
+ var EMPTY_PAGE = Object.freeze({
5213
+ items: Object.freeze([]),
5214
+ total: 0
5215
+ });
5216
+ function paginationCollision(key, op, claimed) {
5217
+ return new AdminError({
5218
+ message: `useResourceList("${key}", { op: "${op}" }): the filter carries ${claimed.join(" and ")}, which the list already sends. The filter is serialised after the pagination, so its value wins on the wire while page.value keeps the one goToPage() set \u2014 the list would report a page it is not showing. Drive the page with goToPage()/setPageSize() and leave those keys out of the filter.`
5219
+ });
5220
+ }
5221
+ function paginationKeysIn(filter) {
5222
+ return LIST_PAGINATION_PARAMS.filter((param) => isSentInQuery(filter[param]));
5223
+ }
5224
+ function readListOp(key, ops, name) {
5225
+ const op = Object.hasOwn(ops, name) ? ops[name] : void 0;
5226
+ if (typeof op !== "function") {
5227
+ throw new Error(
5228
+ `useResourceList("${key}", { op: "${name}" }): that resource has no such operation. It offers: ${Object.keys(ops).join(", ")}.`
5229
+ );
5230
+ }
5231
+ return op;
5232
+ }
5233
+ function useResourceList(key, opts) {
5234
+ const ops = useResource(key);
5235
+ const opName = opts?.op ?? DEFAULT_LIST_OP;
5236
+ const load = readListOp(key, ops, opName);
5237
+ if (opts?.filter) {
5238
+ const claimed = paginationKeysIn(opts.filter.value);
5239
+ if (claimed.length > 0) throw paginationCollision(key, opName, claimed);
5240
+ }
5241
+ const page = (0, import_vue10.ref)(LIST_FIRST_PAGE);
5242
+ const pageSize = (0, import_vue10.ref)(clampListPageSize(opts?.pageSize ?? LIST_PAGE_SIZE_DEFAULT));
5243
+ function query() {
5244
+ const filter = opts?.filter?.value;
5245
+ if (filter) {
5246
+ const claimed = paginationKeysIn(filter);
5247
+ if (claimed.length > 0) throw paginationCollision(key, opName, claimed);
5248
+ }
5249
+ return { page: page.value, pageSize: pageSize.value, filter };
5250
+ }
5251
+ const state = useAsyncData(() => load(query()), {
5252
+ initial: EMPTY_PAGE,
5253
+ immediate: opts?.immediate
5254
+ });
5255
+ (0, import_vue10.watch)(state.data, (answer) => {
5256
+ if (typeof answer.page === "number") page.value = answer.page;
5257
+ if (typeof answer.pageSize === "number") pageSize.value = answer.pageSize;
5258
+ });
5259
+ async function goToPage(next) {
5260
+ page.value = clampListPage(next);
5261
+ await state.reload();
5262
+ }
5263
+ async function setPageSize(size) {
5264
+ pageSize.value = clampListPageSize(size);
5265
+ page.value = LIST_FIRST_PAGE;
5266
+ await state.reload();
5267
+ }
5268
+ if (opts?.filter) {
5269
+ (0, import_vue10.watch)(
5270
+ opts.filter,
5271
+ () => {
5272
+ page.value = LIST_FIRST_PAGE;
5273
+ void state.reload();
5274
+ },
5275
+ { deep: true }
5276
+ );
5277
+ }
5278
+ return {
5279
+ items: (0, import_vue10.computed)(() => state.data.value.items),
5280
+ total: (0, import_vue10.computed)(() => state.data.value.total ?? state.data.value.items.length),
5281
+ page,
5282
+ pageSize,
5283
+ pending: state.pending,
5284
+ error: state.error,
5285
+ reload: state.reload,
5286
+ goToPage,
5287
+ setPageSize
5288
+ };
5289
+ }
5290
+
5291
+ // src/vue/use-tenants.ts
5292
+ var import_vue11 = require("vue");
4409
5293
  function useTenants(options) {
4410
5294
  if (!options?.endpoint) {
4411
5295
  throw new Error(
4412
5296
  'useTenants: `endpoint` is required (e.g. "/api/v1/admin/tenants" or "/api/admin/tenants"). The platform has no default because apps use different globalPrefix conventions.'
4413
5297
  );
4414
5298
  }
4415
- const filter = options.filter ?? (0, import_vue7.ref)({});
5299
+ const filter = options.filter ?? (0, import_vue11.ref)({});
4416
5300
  const list = useApiList({
4417
5301
  endpoint: options.endpoint,
4418
5302
  filter,
@@ -4424,14 +5308,14 @@ function useTenants(options) {
4424
5308
  }
4425
5309
 
4426
5310
  // src/vue/use-audit-entries.ts
4427
- var import_vue8 = require("vue");
5311
+ var import_vue12 = require("vue");
4428
5312
  function useAuditEntries(options) {
4429
5313
  if (!options?.endpoint) {
4430
5314
  throw new Error(
4431
5315
  'useAuditEntries: `endpoint` is required (e.g. "/api/admin/audit" or "/api/v1/admin/audit"). The platform has no default because apps use different globalPrefix conventions.'
4432
5316
  );
4433
5317
  }
4434
- const filter = options.filter ?? (0, import_vue8.ref)({});
5318
+ const filter = options.filter ?? (0, import_vue12.ref)({});
4435
5319
  const list = useApiList({
4436
5320
  endpoint: options.endpoint,
4437
5321
  filter,
@@ -4443,7 +5327,7 @@ function useAuditEntries(options) {
4443
5327
  }
4444
5328
 
4445
5329
  // src/vue/use-entitlement.ts
4446
- var import_vue9 = require("vue");
5330
+ var import_vue13 = require("vue");
4447
5331
  function useEntitlement(options) {
4448
5332
  if (!options?.endpoint) {
4449
5333
  throw new Error(
@@ -4452,9 +5336,9 @@ function useEntitlement(options) {
4452
5336
  }
4453
5337
  const endpoint = options.endpoint;
4454
5338
  const http = options.http ?? defaultHttpClient();
4455
- const entitlement = (0, import_vue9.ref)(null);
4456
- const loading = (0, import_vue9.ref)(false);
4457
- const error = (0, import_vue9.ref)(null);
5339
+ const entitlement = (0, import_vue13.ref)(null);
5340
+ const loading = (0, import_vue13.ref)(false);
5341
+ const error = (0, import_vue13.ref)(null);
4458
5342
  async function load() {
4459
5343
  loading.value = true;
4460
5344
  error.value = null;
@@ -4484,15 +5368,15 @@ function useEntitlement(options) {
4484
5368
  }
4485
5369
 
4486
5370
  // src/vue/use-tenant-manifest.ts
4487
- var import_vue10 = require("vue");
5371
+ var import_vue14 = require("vue");
4488
5372
  function useTenantManifest(options) {
4489
5373
  if (!options?.endpoint) {
4490
5374
  throw new Error('useTenantManifest: `endpoint` is required (e.g. "/api/tenant/manifest").');
4491
5375
  }
4492
5376
  const http = options.http ?? defaultHttpClient();
4493
- const manifest = (0, import_vue10.ref)(null);
4494
- const loading = (0, import_vue10.ref)(false);
4495
- const error = (0, import_vue10.ref)(null);
5377
+ const manifest = (0, import_vue14.ref)(null);
5378
+ const loading = (0, import_vue14.ref)(false);
5379
+ const error = (0, import_vue14.ref)(null);
4496
5380
  async function load() {
4497
5381
  loading.value = true;
4498
5382
  error.value = null;
@@ -4526,7 +5410,7 @@ function useTenantManifest(options) {
4526
5410
  }
4527
5411
 
4528
5412
  // src/vue/entitlement-provider.ts
4529
- var import_vue11 = require("vue");
5413
+ var import_vue15 = require("vue");
4530
5414
  var ENTITLEMENT_INJECTION_KEY = /* @__PURE__ */ Symbol.for(
4531
5415
  "@saasicat/ui-vue/ENTITLEMENT"
4532
5416
  );
@@ -4534,7 +5418,7 @@ function provideEntitlement(app, entitlement) {
4534
5418
  app.provide(ENTITLEMENT_INJECTION_KEY, entitlement);
4535
5419
  }
4536
5420
  function useInjectedEntitlement() {
4537
- return (0, import_vue11.inject)(ENTITLEMENT_INJECTION_KEY, null);
5421
+ return (0, import_vue15.inject)(ENTITLEMENT_INJECTION_KEY, null);
4538
5422
  }
4539
5423
 
4540
5424
  // src/vue/feature-router-guard.ts
@@ -4563,15 +5447,15 @@ function buildFeatureRouterGuard(options) {
4563
5447
  }
4564
5448
 
4565
5449
  // src/vue/use-tenant-billing-catalog.ts
4566
- var import_vue12 = require("vue");
5450
+ var import_vue16 = require("vue");
4567
5451
  function useTenantBillingCatalog(options = {}) {
4568
5452
  const apiPrefix = trimTrailingSlashes(options.apiPrefix ?? "/billing");
4569
5453
  const http = options.http ?? defaultHttpClient();
4570
- const plans = (0, import_vue12.ref)(null);
4571
- const featureRegistry = (0, import_vue12.ref)(null);
4572
- const bundles = (0, import_vue12.ref)(null);
4573
- const loading = (0, import_vue12.ref)(false);
4574
- const error = (0, import_vue12.ref)(null);
5454
+ const plans = (0, import_vue16.ref)(null);
5455
+ const featureRegistry = (0, import_vue16.ref)(null);
5456
+ const bundles = (0, import_vue16.ref)(null);
5457
+ const loading = (0, import_vue16.ref)(false);
5458
+ const error = (0, import_vue16.ref)(null);
4575
5459
  async function fetchJson(path) {
4576
5460
  const res = await http(`${apiPrefix}${path}`, { method: "GET" });
4577
5461
  if (res.status !== 200) {
@@ -4621,14 +5505,14 @@ function useTenantBillingCatalog(options = {}) {
4621
5505
  }
4622
5506
 
4623
5507
  // src/vue/use-tenant-billing.ts
4624
- var import_vue13 = require("vue");
5508
+ var import_vue17 = require("vue");
4625
5509
  function useTenantBilling(options = {}) {
4626
5510
  const apiPrefix = trimTrailingSlashes(options.apiPrefix ?? "/billing");
4627
5511
  const http = options.http ?? defaultHttpClient();
4628
- const usage = (0, import_vue13.ref)(null);
4629
- const loading = (0, import_vue13.ref)(false);
4630
- const error = (0, import_vue13.ref)(null);
4631
- const subscriptionBundles = (0, import_vue13.ref)([]);
5512
+ const usage = (0, import_vue17.ref)(null);
5513
+ const loading = (0, import_vue17.ref)(false);
5514
+ const error = (0, import_vue17.ref)(null);
5515
+ const subscriptionBundles = (0, import_vue17.ref)([]);
4632
5516
  function authHeaders() {
4633
5517
  const headers = {};
4634
5518
  const token = options.getAuthToken?.();
@@ -4762,7 +5646,7 @@ function useTenantBilling(options = {}) {
4762
5646
  }
4763
5647
 
4764
5648
  // src/vue/use-subscription-draft.ts
4765
- var import_vue14 = require("vue");
5649
+ var import_vue18 = require("vue");
4766
5650
  var import_types13 = require("@saasicat/types");
4767
5651
  var DEFAULT_YEARLY_FACTOR = 10;
4768
5652
  function unwrap(source) {
@@ -4779,27 +5663,27 @@ function priceForCycle(monthlyNet, yearlyNet, cycle, yearlyFactor) {
4779
5663
  }
4780
5664
  function useSubscriptionDraft(options) {
4781
5665
  const yearlyFactor = options.yearlyFactor ?? DEFAULT_YEARLY_FACTOR;
4782
- const plansRef = (0, import_vue14.computed)(() => unwrap(options.plans) ?? []);
4783
- const subscriptionBundlesRef = (0, import_vue14.computed)(
5666
+ const plansRef = (0, import_vue18.computed)(() => unwrap(options.plans) ?? []);
5667
+ const subscriptionBundlesRef = (0, import_vue18.computed)(
4784
5668
  () => unwrap(options.subscriptionBundles ?? null) ?? []
4785
5669
  );
4786
- const plan = (0, import_vue14.ref)(options.initialPlan ?? null);
4787
- const cycle = (0, import_vue14.ref)(options.initialCycle ?? "MONTHLY");
4788
- const selectedBundleVersionIds = (0, import_vue14.ref)(
5670
+ const plan = (0, import_vue18.ref)(options.initialPlan ?? null);
5671
+ const cycle = (0, import_vue18.ref)(options.initialCycle ?? "MONTHLY");
5672
+ const selectedBundleVersionIds = (0, import_vue18.ref)(
4789
5673
  new Set(options.initialBundleVersionIds ?? [])
4790
5674
  );
4791
- const promoCode = (0, import_vue14.ref)("");
4792
- const promoState = (0, import_vue14.ref)({ status: "idle", preview: null, message: "" });
4793
- const selectedPlan = (0, import_vue14.computed)(() => {
5675
+ const promoCode = (0, import_vue18.ref)("");
5676
+ const promoState = (0, import_vue18.ref)({ status: "idle", preview: null, message: "" });
5677
+ const selectedPlan = (0, import_vue18.computed)(() => {
4794
5678
  if (!plan.value) return null;
4795
5679
  return plansRef.value.find((p) => p.id === plan.value) ?? null;
4796
5680
  });
4797
- const selectedBundles = (0, import_vue14.computed)(
5681
+ const selectedBundles = (0, import_vue18.computed)(
4798
5682
  () => subscriptionBundlesRef.value.filter(
4799
5683
  (b) => selectedBundleVersionIds.value.has(b.bundleVersionId)
4800
5684
  )
4801
5685
  );
4802
- const chargeableBundles = (0, import_vue14.computed)(() => {
5686
+ const chargeableBundles = (0, import_vue18.computed)(() => {
4803
5687
  const planFeatures = selectedPlan.value?.features ?? [];
4804
5688
  const selected = selectedBundles.value;
4805
5689
  const keptIds = new Set(
@@ -4807,7 +5691,7 @@ function useSubscriptionDraft(options) {
4807
5691
  );
4808
5692
  return selected.filter((b) => keptIds.has(b.bundleVersionId));
4809
5693
  });
4810
- const activeFeatures = (0, import_vue14.computed)(() => {
5694
+ const activeFeatures = (0, import_vue18.computed)(() => {
4811
5695
  const result = /* @__PURE__ */ new Set();
4812
5696
  const planObj = selectedPlan.value;
4813
5697
  if (planObj) {
@@ -4818,7 +5702,7 @@ function useSubscriptionDraft(options) {
4818
5702
  }
4819
5703
  return result;
4820
5704
  });
4821
- const pricing = (0, import_vue14.computed)(() => {
5705
+ const pricing = (0, import_vue18.computed)(() => {
4822
5706
  const cyc = cycle.value;
4823
5707
  const planObj = selectedPlan.value;
4824
5708
  const planNet = planObj ? priceForCycle(planObj.monthlyNet ?? 0, planObj.yearlyNet, cyc, yearlyFactor) : 0;
@@ -4878,7 +5762,7 @@ function useSubscriptionDraft(options) {
4878
5762
  }
4879
5763
  };
4880
5764
  });
4881
- const isDirty = (0, import_vue14.computed)(() => {
5765
+ const isDirty = (0, import_vue18.computed)(() => {
4882
5766
  return selectedBundleVersionIds.value.size > 0 || promoState.value.status === "valid";
4883
5767
  });
4884
5768
  function setPlan(planId) {
@@ -4954,7 +5838,7 @@ function useSubscriptionDraft(options) {
4954
5838
  }
4955
5839
 
4956
5840
  // src/vue/use-bulk-publish.ts
4957
- var import_vue15 = require("vue");
5841
+ var import_vue19 = require("vue");
4958
5842
  function useBulkPublish(options) {
4959
5843
  const msg = useSaMessages("planVersions");
4960
5844
  if (!options?.endpoints) {
@@ -4964,15 +5848,15 @@ function useBulkPublish(options) {
4964
5848
  }
4965
5849
  const http = options.http ?? defaultHttpClient();
4966
5850
  const endpoints = options.endpoints;
4967
- const items = (0, import_vue15.ref)([]);
4968
- const successCount = (0, import_vue15.computed)(() => items.value.filter((i) => i.status === "published").length);
4969
- const failureCount = (0, import_vue15.computed)(() => items.value.filter((i) => i.status === "failed").length);
4970
- const progress = (0, import_vue15.computed)(() => {
5851
+ const items = (0, import_vue19.ref)([]);
5852
+ const successCount = (0, import_vue19.computed)(() => items.value.filter((i) => i.status === "published").length);
5853
+ const failureCount = (0, import_vue19.computed)(() => items.value.filter((i) => i.status === "failed").length);
5854
+ const progress = (0, import_vue19.computed)(() => {
4971
5855
  if (items.value.length === 0) return 0;
4972
5856
  const done2 = successCount.value + failureCount.value;
4973
5857
  return done2 / items.value.length;
4974
5858
  });
4975
- const done = (0, import_vue15.computed)(
5859
+ const done = (0, import_vue19.computed)(
4976
5860
  () => items.value.length > 0 && items.value.every((i) => i.status === "published" || i.status === "failed")
4977
5861
  );
4978
5862
  function setItems(next) {
@@ -5030,7 +5914,7 @@ function useBulkPublish(options) {
5030
5914
  }
5031
5915
 
5032
5916
  // src/vue/use-plan-editor.ts
5033
- var import_vue16 = require("vue");
5917
+ var import_vue20 = require("vue");
5034
5918
  var PlannedOnlyFeatureError = class extends Error {
5035
5919
  constructor(violations) {
5036
5920
  super(
@@ -5050,10 +5934,10 @@ function tierWeight(tier) {
5050
5934
  function usePlanEditor(manifest, options = {}) {
5051
5935
  const baseSet = new Set(options.baseFeatures ?? []);
5052
5936
  const nonRegressive = options.nonRegressive ?? true;
5053
- const selectedFeatures = (0, import_vue16.ref)(new Set(options.initialFeatures ?? []));
5937
+ const selectedFeatures = (0, import_vue20.ref)(new Set(options.initialFeatures ?? []));
5054
5938
  const catalogFeatures = manifest.planCatalogSnapshot.features ?? [];
5055
5939
  const plannedOnlySet = new Set(catalogFeatures.filter((f) => f.plannedOnly).map((f) => f.key));
5056
- const availableFeatures = (0, import_vue16.computed)(() => {
5940
+ const availableFeatures = (0, import_vue20.computed)(() => {
5057
5941
  return catalogFeatures.map((def) => {
5058
5942
  const isSelected = selectedFeatures.value.has(def.key);
5059
5943
  const isPlannedOnly = !!def.plannedOnly;
@@ -5062,7 +5946,7 @@ function usePlanEditor(manifest, options = {}) {
5062
5946
  return { def, isSelected, isPlannedOnly, isInherited, canToggle };
5063
5947
  });
5064
5948
  });
5065
- const featuresByTier = (0, import_vue16.computed)(() => {
5949
+ const featuresByTier = (0, import_vue20.computed)(() => {
5066
5950
  const groups = /* @__PURE__ */ new Map();
5067
5951
  for (const row of availableFeatures.value) {
5068
5952
  const tier = row.def.tier ?? "OTHER";
@@ -5107,12 +5991,12 @@ function usePlanEditor(manifest, options = {}) {
5107
5991
  }
5108
5992
 
5109
5993
  // src/vue/use-public-boot.ts
5110
- var import_vue17 = require("vue");
5994
+ var import_vue21 = require("vue");
5111
5995
  function usePublicBoot(options) {
5112
5996
  const loader = new BootLoader(options);
5113
- const boot = (0, import_vue17.ref)(null);
5114
- const loading = (0, import_vue17.ref)(false);
5115
- const error = (0, import_vue17.ref)(null);
5997
+ const boot = (0, import_vue21.ref)(null);
5998
+ const loading = (0, import_vue21.ref)(false);
5999
+ const error = (0, import_vue21.ref)(null);
5116
6000
  async function load() {
5117
6001
  loading.value = true;
5118
6002
  error.value = null;
@@ -5129,27 +6013,27 @@ function usePublicBoot(options) {
5129
6013
  }
5130
6014
 
5131
6015
  // src/vue/use-discovery.ts
5132
- var import_vue18 = require("vue");
6016
+ var import_vue22 = require("vue");
5133
6017
  var DiscoveryLoadError = class extends Error {
5134
6018
  constructor(status, message) {
5135
6019
  super(message);
5136
6020
  this.status = status;
5137
6021
  this.name = "DiscoveryLoadError";
6022
+ markPlatformError(this);
5138
6023
  }
5139
6024
  status;
5140
6025
  };
5141
6026
  function useDiscovery(options) {
5142
- const msg = useSaMessages("discovery");
5143
6027
  if (!options?.endpoint) {
5144
6028
  throw new Error(
5145
6029
  'useDiscovery: `endpoint` is required (e.g. "/api/admin/discovery" or "/api/v1/admin/discovery"). The platform has no default because apps use different globalPrefix conventions.'
5146
6030
  );
5147
6031
  }
5148
6032
  const http = options.http ?? defaultHttpClient();
5149
- const snapshot = (0, import_vue18.ref)(null);
5150
- const etag = (0, import_vue18.ref)(null);
5151
- const loading = (0, import_vue18.ref)(false);
5152
- const error = (0, import_vue18.ref)(null);
6033
+ const snapshot = (0, import_vue22.ref)(null);
6034
+ const etag = (0, import_vue22.ref)(null);
6035
+ const loading = (0, import_vue22.ref)(false);
6036
+ const error = (0, import_vue22.ref)(null);
5153
6037
  async function fetchSnapshot(includeEtag) {
5154
6038
  loading.value = true;
5155
6039
  error.value = null;
@@ -5161,13 +6045,19 @@ function useDiscovery(options) {
5161
6045
  headers["If-None-Match"] = etag.value;
5162
6046
  }
5163
6047
  const res = await http(options.endpoint, { method: "GET", headers });
6048
+ requireServerAnswer(
6049
+ res.status,
6050
+ "GET",
6051
+ options.endpoint,
6052
+ (diagnostic) => new DiscoveryLoadError(res.status, diagnostic)
6053
+ );
5164
6054
  if (res.status === 304) {
5165
6055
  return;
5166
6056
  }
5167
6057
  if (res.status !== 200) {
5168
6058
  throw new DiscoveryLoadError(
5169
6059
  res.status,
5170
- formatMessage(msg.value.errorDiscoveryHttp, { status: res.status })
6060
+ `Discovery endpoint responded with HTTP ${res.status}`
5171
6061
  );
5172
6062
  }
5173
6063
  const body = await res.json();
@@ -5193,11 +6083,18 @@ function useDiscovery(options) {
5193
6083
  const headers = { "content-type": "application/json" };
5194
6084
  const token = options.getAuthToken?.();
5195
6085
  if (token) headers.Authorization = `Bearer ${token}`;
5196
- const res = await http(`${options.endpoint}/rescan`, { method: "POST", headers });
6086
+ const rescanUrl = `${options.endpoint}/rescan`;
6087
+ const res = await http(rescanUrl, { method: "POST", headers });
6088
+ requireServerAnswer(
6089
+ res.status,
6090
+ "POST",
6091
+ rescanUrl,
6092
+ (diagnostic) => new DiscoveryLoadError(res.status, diagnostic)
6093
+ );
5197
6094
  if (res.status !== 200 && res.status !== 201) {
5198
6095
  throw new DiscoveryLoadError(
5199
6096
  res.status,
5200
- formatMessage(msg.value.errorRescanHttp, { status: res.status })
6097
+ `Discovery rescan responded with HTTP ${res.status}`
5201
6098
  );
5202
6099
  }
5203
6100
  const body = await res.json();
@@ -5216,13 +6113,14 @@ function useDiscovery(options) {
5216
6113
  }
5217
6114
 
5218
6115
  // src/vue/use-catalog-entries.ts
5219
- var import_vue19 = require("vue");
6116
+ var import_vue23 = require("vue");
5220
6117
  var CatalogEntriesApiError = class extends Error {
5221
6118
  constructor(status, body, message) {
5222
6119
  super(message);
5223
6120
  this.status = status;
5224
6121
  this.body = body;
5225
6122
  this.name = "CatalogEntriesApiError";
6123
+ markPlatformError(this);
5226
6124
  }
5227
6125
  status;
5228
6126
  body;
@@ -5234,13 +6132,12 @@ function useCatalogEntries(options) {
5234
6132
  if (!options?.projectKey) {
5235
6133
  throw new Error("useCatalogEntries: `projectKey` is required.");
5236
6134
  }
5237
- const msg = useSaMessages("discovery");
5238
6135
  const http = options.http ?? defaultHttpClient();
5239
- const capabilities = (0, import_vue19.ref)([]);
5240
- const features = (0, import_vue19.ref)([]);
5241
- const quotas = (0, import_vue19.ref)([]);
5242
- const loading = (0, import_vue19.ref)(false);
5243
- const error = (0, import_vue19.ref)(null);
6136
+ const capabilities = (0, import_vue23.ref)([]);
6137
+ const features = (0, import_vue23.ref)([]);
6138
+ const quotas = (0, import_vue23.ref)([]);
6139
+ const loading = (0, import_vue23.ref)(false);
6140
+ const error = (0, import_vue23.ref)(null);
5244
6141
  const base = `${options.adminEndpoint}/catalog`;
5245
6142
  const pk = encodeURIComponent(options.projectKey);
5246
6143
  function authHeaders() {
@@ -5248,18 +6145,25 @@ function useCatalogEntries(options) {
5248
6145
  return token ? { Authorization: `Bearer ${token}` } : {};
5249
6146
  }
5250
6147
  async function fetchJson(url, init) {
6148
+ const method = init?.method ?? "GET";
5251
6149
  const res = await http(url, {
5252
- method: init?.method ?? "GET",
6150
+ method,
5253
6151
  headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
5254
6152
  body: init?.body
5255
6153
  });
6154
+ requireServerAnswer(
6155
+ res.status,
6156
+ method,
6157
+ url,
6158
+ (diagnostic) => new CatalogEntriesApiError(res.status, null, diagnostic)
6159
+ );
5256
6160
  if (res.status === 204) return null;
5257
6161
  const body = await res.json().catch(() => null);
5258
6162
  if (res.status >= 400) {
5259
6163
  throw new CatalogEntriesApiError(
5260
6164
  res.status,
5261
6165
  body,
5262
- formatMessage(msg.value.errorCatalogHttp, { status: res.status })
6166
+ `Catalog entries API responded with HTTP ${res.status}`
5263
6167
  );
5264
6168
  }
5265
6169
  return body;
@@ -5287,7 +6191,8 @@ function useCatalogEntries(options) {
5287
6191
  `${base}/features/${encodeURIComponent(featureKey)}/review?projectKey=${pk}`,
5288
6192
  { method: "PATCH", body: JSON.stringify(data) }
5289
6193
  );
5290
- if (!updated) throw new CatalogEntriesApiError(0, null, "Review returned no body");
6194
+ if (!updated)
6195
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "Review returned no body"));
5291
6196
  features.value = features.value.map((f) => f.featureKey === featureKey ? updated : f);
5292
6197
  return updated;
5293
6198
  }
@@ -5296,7 +6201,8 @@ function useCatalogEntries(options) {
5296
6201
  `${base}/quotas/${encodeURIComponent(quotaKey)}/review?projectKey=${pk}`,
5297
6202
  { method: "PATCH", body: JSON.stringify(data) }
5298
6203
  );
5299
- if (!updated) throw new CatalogEntriesApiError(0, null, "Review returned no body");
6204
+ if (!updated)
6205
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "Review returned no body"));
5300
6206
  quotas.value = quotas.value.map((q) => q.quotaKey === quotaKey ? updated : q);
5301
6207
  return updated;
5302
6208
  }
@@ -5305,7 +6211,8 @@ function useCatalogEntries(options) {
5305
6211
  `${base}/features/${encodeURIComponent(featureKey)}/i18n?projectKey=${pk}`,
5306
6212
  { method: "PATCH", body: JSON.stringify({ i18n }) }
5307
6213
  );
5308
- if (!updated) throw new CatalogEntriesApiError(0, null, "i18n returned no body");
6214
+ if (!updated)
6215
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "i18n returned no body"));
5309
6216
  features.value = features.value.map((f) => f.featureKey === featureKey ? updated : f);
5310
6217
  return updated;
5311
6218
  }
@@ -5314,7 +6221,8 @@ function useCatalogEntries(options) {
5314
6221
  `${base}/quotas/${encodeURIComponent(quotaKey)}/i18n?projectKey=${pk}`,
5315
6222
  { method: "PATCH", body: JSON.stringify({ i18n }) }
5316
6223
  );
5317
- if (!updated) throw new CatalogEntriesApiError(0, null, "i18n returned no body");
6224
+ if (!updated)
6225
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "i18n returned no body"));
5318
6226
  quotas.value = quotas.value.map((q) => q.quotaKey === quotaKey ? updated : q);
5319
6227
  return updated;
5320
6228
  }
@@ -5323,7 +6231,8 @@ function useCatalogEntries(options) {
5323
6231
  `${base}/features/${encodeURIComponent(featureKey)}?projectKey=${pk}`,
5324
6232
  { method: "PATCH", body: JSON.stringify(data) }
5325
6233
  );
5326
- if (!updated) throw new CatalogEntriesApiError(0, null, "Base returned no body");
6234
+ if (!updated)
6235
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "Base returned no body"));
5327
6236
  features.value = features.value.map((f) => f.featureKey === featureKey ? updated : f);
5328
6237
  return updated;
5329
6238
  }
@@ -5332,7 +6241,8 @@ function useCatalogEntries(options) {
5332
6241
  `${base}/quotas/${encodeURIComponent(quotaKey)}?projectKey=${pk}`,
5333
6242
  { method: "PATCH", body: JSON.stringify(data) }
5334
6243
  );
5335
- if (!updated) throw new CatalogEntriesApiError(0, null, "Base returned no body");
6244
+ if (!updated)
6245
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "Base returned no body"));
5336
6246
  quotas.value = quotas.value.map((q) => q.quotaKey === quotaKey ? updated : q);
5337
6247
  return updated;
5338
6248
  }
@@ -5341,7 +6251,8 @@ function useCatalogEntries(options) {
5341
6251
  method: "POST",
5342
6252
  body: JSON.stringify({ snapshot })
5343
6253
  });
5344
- if (!result) throw new CatalogEntriesApiError(0, null, "Sync returned no body");
6254
+ if (!result)
6255
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "Sync returned no body"));
5345
6256
  await load();
5346
6257
  return result;
5347
6258
  }
@@ -5366,13 +6277,14 @@ function useCatalogEntries(options) {
5366
6277
  }
5367
6278
 
5368
6279
  // src/vue/use-bundles.ts
5369
- var import_vue20 = require("vue");
6280
+ var import_vue24 = require("vue");
5370
6281
  var BundlesApiError = class extends Error {
5371
6282
  constructor(status, body, message) {
5372
6283
  super(message);
5373
6284
  this.status = status;
5374
6285
  this.body = body;
5375
6286
  this.name = "BundlesApiError";
6287
+ markPlatformError(this);
5376
6288
  }
5377
6289
  status;
5378
6290
  body;
@@ -5387,20 +6299,27 @@ function useBundles(options) {
5387
6299
  throw new Error("useBundles: `projectKey` is required.");
5388
6300
  }
5389
6301
  const http = options.http ?? defaultHttpClient();
5390
- const bundles = (0, import_vue20.ref)([]);
5391
- const loading = (0, import_vue20.ref)(false);
5392
- const error = (0, import_vue20.ref)(null);
6302
+ const bundles = (0, import_vue24.ref)([]);
6303
+ const loading = (0, import_vue24.ref)(false);
6304
+ const error = (0, import_vue24.ref)(null);
5393
6305
  const baseUrl = `${options.adminEndpoint}/catalog/bundles`;
5394
6306
  function authHeaders() {
5395
6307
  const token = options.getAuthToken?.();
5396
6308
  return token ? { Authorization: `Bearer ${token}` } : {};
5397
6309
  }
5398
6310
  async function fetchJson(url, init) {
6311
+ const method = init?.method ?? "GET";
5399
6312
  const res = await http(url, {
5400
- method: init?.method ?? "GET",
6313
+ method,
5401
6314
  headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
5402
6315
  body: init?.body
5403
6316
  });
6317
+ requireServerAnswer(
6318
+ res.status,
6319
+ method,
6320
+ url,
6321
+ (diagnostic) => new BundlesApiError(res.status, null, diagnostic)
6322
+ );
5404
6323
  if (res.status === 204) return null;
5405
6324
  const body = await res.json().catch(() => null);
5406
6325
  if (res.status >= 400) {
@@ -5431,7 +6350,8 @@ function useBundles(options) {
5431
6350
  method: "POST",
5432
6351
  body: JSON.stringify(data)
5433
6352
  });
5434
- if (!created) throw new BundlesApiError(0, null, "Create returned no body");
6353
+ if (!created)
6354
+ throw markEmptyResponse(new BundlesApiError(0, null, "Create returned no body"));
5435
6355
  bundles.value = [...bundles.value, created];
5436
6356
  return created;
5437
6357
  }
@@ -5440,7 +6360,8 @@ function useBundles(options) {
5440
6360
  method: "PATCH",
5441
6361
  body: JSON.stringify(data)
5442
6362
  });
5443
- if (!updated) throw new BundlesApiError(0, null, "Update returned no body");
6363
+ if (!updated)
6364
+ throw markEmptyResponse(new BundlesApiError(0, null, "Update returned no body"));
5444
6365
  bundles.value = bundles.value.map((b) => b.id === bundleId ? updated : b);
5445
6366
  return updated;
5446
6367
  }
@@ -5461,9 +6382,9 @@ function useBundleVersions(options) {
5461
6382
  throw new Error("useBundleVersions: `bundleId` is required.");
5462
6383
  }
5463
6384
  const http = options.http ?? defaultHttpClient();
5464
- const versions = (0, import_vue20.ref)([]);
5465
- const loading = (0, import_vue20.ref)(false);
5466
- const error = (0, import_vue20.ref)(null);
6385
+ const versions = (0, import_vue24.ref)([]);
6386
+ const loading = (0, import_vue24.ref)(false);
6387
+ const error = (0, import_vue24.ref)(null);
5467
6388
  const bundleVersionsUrl = `${options.adminEndpoint}/catalog/bundles/${options.bundleId}/versions`;
5468
6389
  const versionUrlBase = `${options.adminEndpoint}/catalog/bundle-versions`;
5469
6390
  function authHeaders() {
@@ -5471,11 +6392,18 @@ function useBundleVersions(options) {
5471
6392
  return token ? { Authorization: `Bearer ${token}` } : {};
5472
6393
  }
5473
6394
  async function fetchJson(url, init) {
6395
+ const method = init?.method ?? "GET";
5474
6396
  const res = await http(url, {
5475
- method: init?.method ?? "GET",
6397
+ method,
5476
6398
  headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
5477
6399
  body: init?.body
5478
6400
  });
6401
+ requireServerAnswer(
6402
+ res.status,
6403
+ method,
6404
+ url,
6405
+ (diagnostic) => new BundlesApiError(res.status, null, diagnostic)
6406
+ );
5479
6407
  if (res.status === 204) return null;
5480
6408
  const body = await res.json().catch(() => null);
5481
6409
  if (res.status >= 400) {
@@ -5504,7 +6432,8 @@ function useBundleVersions(options) {
5504
6432
  method: "POST",
5505
6433
  body: JSON.stringify(data)
5506
6434
  });
5507
- if (!result) throw new BundlesApiError(0, null, "CreateDraft returned no body");
6435
+ if (!result)
6436
+ throw markEmptyResponse(new BundlesApiError(0, null, "CreateDraft returned no body"));
5508
6437
  versions.value = [...versions.value, result.bundleVersion];
5509
6438
  return result;
5510
6439
  }
@@ -5513,7 +6442,8 @@ function useBundleVersions(options) {
5513
6442
  `${versionUrlBase}/${versionId}`,
5514
6443
  { method: "PATCH", body: JSON.stringify(data) }
5515
6444
  );
5516
- if (!result) throw new BundlesApiError(0, null, "UpdateDraft returned no body");
6445
+ if (!result)
6446
+ throw markEmptyResponse(new BundlesApiError(0, null, "UpdateDraft returned no body"));
5517
6447
  versions.value = versions.value.map((v) => v.id === versionId ? result.bundleVersion : v);
5518
6448
  return result;
5519
6449
  }
@@ -5522,7 +6452,8 @@ function useBundleVersions(options) {
5522
6452
  `${versionUrlBase}/${versionId}/publish`,
5523
6453
  { method: "POST", body: JSON.stringify(opts) }
5524
6454
  );
5525
- if (!result) throw new BundlesApiError(0, null, "Publish returned no body");
6455
+ if (!result)
6456
+ throw markEmptyResponse(new BundlesApiError(0, null, "Publish returned no body"));
5526
6457
  await load();
5527
6458
  return result;
5528
6459
  }
@@ -5537,7 +6468,7 @@ function useBundleVersions(options) {
5537
6468
  }
5538
6469
 
5539
6470
  // src/vue/use-bundle-versions-map.ts
5540
- var import_vue21 = require("vue");
6471
+ var import_vue25 = require("vue");
5541
6472
  function useBundleVersionsMap(options) {
5542
6473
  if (!options?.adminEndpoint) {
5543
6474
  throw new Error("useBundleVersionsMap: `adminEndpoint` is required.");
@@ -5546,9 +6477,9 @@ function useBundleVersionsMap(options) {
5546
6477
  throw new Error("useBundleVersionsMap: `bundles` is required.");
5547
6478
  }
5548
6479
  const http = options.http ?? defaultHttpClient();
5549
- const versionsByBundle = (0, import_vue21.ref)({});
5550
- const loading = (0, import_vue21.ref)(false);
5551
- const error = (0, import_vue21.ref)(null);
6480
+ const versionsByBundle = (0, import_vue25.ref)({});
6481
+ const loading = (0, import_vue25.ref)(false);
6482
+ const error = (0, import_vue25.ref)(null);
5552
6483
  function authHeaders() {
5553
6484
  const token = options.getAuthToken?.();
5554
6485
  return token ? { Authorization: `Bearer ${token}` } : {};
@@ -5598,7 +6529,7 @@ function useBundleVersionsMap(options) {
5598
6529
  console.warn(`useBundleVersionsMap: refreshOne('${bundleId}') failed`, err);
5599
6530
  }
5600
6531
  }
5601
- (0, import_vue21.watch)(
6532
+ (0, import_vue25.watch)(
5602
6533
  () => options.bundles.value.map((b) => b.id).join(","),
5603
6534
  () => {
5604
6535
  if (options.bundles.value.length > 0) void refresh();
@@ -5610,13 +6541,14 @@ function useBundleVersionsMap(options) {
5610
6541
  }
5611
6542
 
5612
6543
  // src/vue/use-tenant-subscription-bundles.ts
5613
- var import_vue22 = require("vue");
6544
+ var import_vue26 = require("vue");
5614
6545
  var TenantSubscriptionBundlesApiError = class extends Error {
5615
6546
  constructor(status, body, message) {
5616
6547
  super(message);
5617
6548
  this.status = status;
5618
6549
  this.body = body;
5619
6550
  this.name = "TenantSubscriptionBundlesApiError";
6551
+ markPlatformError(this);
5620
6552
  }
5621
6553
  status;
5622
6554
  body;
@@ -5629,19 +6561,26 @@ function useTenantSubscriptionBundles(options) {
5629
6561
  }
5630
6562
  const http = options.http ?? defaultHttpClient();
5631
6563
  const baseUrl = `${options.billingEndpoint}/billing/subscription-bundles`;
5632
- const bundles = (0, import_vue22.ref)([]);
5633
- const loading = (0, import_vue22.ref)(false);
5634
- const error = (0, import_vue22.ref)(null);
6564
+ const bundles = (0, import_vue26.ref)([]);
6565
+ const loading = (0, import_vue26.ref)(false);
6566
+ const error = (0, import_vue26.ref)(null);
5635
6567
  function authHeaders() {
5636
6568
  const token = options.getAuthToken?.();
5637
6569
  return token ? { Authorization: `Bearer ${token}` } : {};
5638
6570
  }
5639
6571
  async function fetchJson(url, init) {
6572
+ const method = init?.method ?? "GET";
5640
6573
  const res = await http(url, {
5641
- method: init?.method ?? "GET",
6574
+ method,
5642
6575
  headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
5643
6576
  body: init?.body
5644
6577
  });
6578
+ requireServerAnswer(
6579
+ res.status,
6580
+ method,
6581
+ url,
6582
+ (diagnostic) => new TenantSubscriptionBundlesApiError(res.status, null, diagnostic)
6583
+ );
5645
6584
  if (res.status === 204) return null;
5646
6585
  const body = await res.json().catch(() => null);
5647
6586
  if (res.status >= 400) {
@@ -5671,7 +6610,9 @@ function useTenantSubscriptionBundles(options) {
5671
6610
  body: JSON.stringify(data)
5672
6611
  });
5673
6612
  if (!result) {
5674
- throw new TenantSubscriptionBundlesApiError(0, null, "add returned no body");
6613
+ throw markEmptyResponse(
6614
+ new TenantSubscriptionBundlesApiError(0, null, "add returned no body")
6615
+ );
5675
6616
  }
5676
6617
  const hydrated = rehydrateDates(result);
5677
6618
  bundles.value = [hydrated, ...bundles.value];
@@ -5683,7 +6624,9 @@ function useTenantSubscriptionBundles(options) {
5683
6624
  { method: "DELETE", body: JSON.stringify(opts) }
5684
6625
  );
5685
6626
  if (!result) {
5686
- throw new TenantSubscriptionBundlesApiError(0, null, "cancel returned no body");
6627
+ throw markEmptyResponse(
6628
+ new TenantSubscriptionBundlesApiError(0, null, "cancel returned no body")
6629
+ );
5687
6630
  }
5688
6631
  const hydrated = rehydrateDates(result);
5689
6632
  bundles.value = bundles.value.map((b) => b.id === subscriptionBundleId ? hydrated : b);
@@ -5705,13 +6648,14 @@ function rehydrateDates(raw) {
5705
6648
  }
5706
6649
 
5707
6650
  // src/vue/use-marketing-projections.ts
5708
- var import_vue23 = require("vue");
6651
+ var import_vue27 = require("vue");
5709
6652
  var MarketingProjectionsApiError = class extends Error {
5710
6653
  constructor(status, body, message) {
5711
6654
  super(message);
5712
6655
  this.status = status;
5713
6656
  this.body = body;
5714
6657
  this.name = "MarketingProjectionsApiError";
6658
+ markPlatformError(this);
5715
6659
  }
5716
6660
  status;
5717
6661
  body;
@@ -5723,12 +6667,11 @@ function useMarketingProjections(options) {
5723
6667
  if (!options?.filter?.projectKey) {
5724
6668
  throw new Error("useMarketingProjections: `filter.projectKey` is required.");
5725
6669
  }
5726
- const msg = useSaMessages("marketing");
5727
6670
  const http = options.http ?? defaultHttpClient();
5728
- const projections = (0, import_vue23.ref)([]);
5729
- const filter = (0, import_vue23.ref)({ ...options.filter });
5730
- const loading = (0, import_vue23.ref)(false);
5731
- const error = (0, import_vue23.ref)(null);
6671
+ const projections = (0, import_vue27.ref)([]);
6672
+ const filter = (0, import_vue27.ref)({ ...options.filter });
6673
+ const loading = (0, import_vue27.ref)(false);
6674
+ const error = (0, import_vue27.ref)(null);
5732
6675
  const baseUrl = `${options.adminEndpoint}/catalog/marketing-projections`;
5733
6676
  function authHeaders() {
5734
6677
  const token = options.getAuthToken?.();
@@ -5744,18 +6687,25 @@ function useMarketingProjections(options) {
5744
6687
  return `${baseUrl}?${params.toString()}`;
5745
6688
  }
5746
6689
  async function fetchJson(url, init) {
6690
+ const method = init?.method ?? "GET";
5747
6691
  const res = await http(url, {
5748
- method: init?.method ?? "GET",
6692
+ method,
5749
6693
  headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
5750
6694
  body: init?.body
5751
6695
  });
6696
+ requireServerAnswer(
6697
+ res.status,
6698
+ method,
6699
+ url,
6700
+ (diagnostic) => new MarketingProjectionsApiError(res.status, null, diagnostic)
6701
+ );
5752
6702
  if (res.status === 204) return null;
5753
6703
  const body = await res.json().catch(() => null);
5754
6704
  if (res.status >= 400) {
5755
6705
  throw new MarketingProjectionsApiError(
5756
6706
  res.status,
5757
6707
  body,
5758
- formatMessage(msg.value.errors.projectionsApi, { status: res.status })
6708
+ `Marketing projections API responded with HTTP ${res.status}`
5759
6709
  );
5760
6710
  }
5761
6711
  return body;
@@ -5782,7 +6732,9 @@ function useMarketingProjections(options) {
5782
6732
  body: JSON.stringify(data)
5783
6733
  });
5784
6734
  if (!created) {
5785
- throw new MarketingProjectionsApiError(0, null, "Create returned no body");
6735
+ throw markEmptyResponse(
6736
+ new MarketingProjectionsApiError(0, null, "Create returned no body")
6737
+ );
5786
6738
  }
5787
6739
  await load();
5788
6740
  return created;
@@ -5793,7 +6745,9 @@ function useMarketingProjections(options) {
5793
6745
  body: JSON.stringify(data)
5794
6746
  });
5795
6747
  if (!updated) {
5796
- throw new MarketingProjectionsApiError(0, null, "Update returned no body");
6748
+ throw markEmptyResponse(
6749
+ new MarketingProjectionsApiError(0, null, "Update returned no body")
6750
+ );
5797
6751
  }
5798
6752
  projections.value = projections.value.map((p) => p.id === id ? updated : p);
5799
6753
  return updated;
@@ -5809,13 +6763,14 @@ function useMarketingProjections(options) {
5809
6763
  }
5810
6764
 
5811
6765
  // src/vue/use-promotions.ts
5812
- var import_vue24 = require("vue");
6766
+ var import_vue28 = require("vue");
5813
6767
  var PromotionsApiError = class extends Error {
5814
6768
  constructor(status, body, message) {
5815
6769
  super(message);
5816
6770
  this.status = status;
5817
6771
  this.body = body;
5818
6772
  this.name = "PromotionsApiError";
6773
+ markPlatformError(this);
5819
6774
  }
5820
6775
  status;
5821
6776
  body;
@@ -5827,11 +6782,10 @@ function usePromotions(options) {
5827
6782
  if (!options?.projectKey) {
5828
6783
  throw new Error("usePromotions: `projectKey` is required.");
5829
6784
  }
5830
- const msg = useSaMessages("promos");
5831
6785
  const http = options.http ?? defaultHttpClient();
5832
- const promotions = (0, import_vue24.ref)([]);
5833
- const loading = (0, import_vue24.ref)(false);
5834
- const error = (0, import_vue24.ref)(null);
6786
+ const promotions = (0, import_vue28.ref)([]);
6787
+ const loading = (0, import_vue28.ref)(false);
6788
+ const error = (0, import_vue28.ref)(null);
5835
6789
  const baseUrl = `${options.adminEndpoint}/catalog/promotions`;
5836
6790
  const pk = encodeURIComponent(options.projectKey);
5837
6791
  function authHeaders() {
@@ -5839,18 +6793,25 @@ function usePromotions(options) {
5839
6793
  return token ? { Authorization: `Bearer ${token}` } : {};
5840
6794
  }
5841
6795
  async function fetchJson(url, init) {
6796
+ const method = init?.method ?? "GET";
5842
6797
  const res = await http(url, {
5843
- method: init?.method ?? "GET",
6798
+ method,
5844
6799
  headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
5845
6800
  body: init?.body
5846
6801
  });
6802
+ requireServerAnswer(
6803
+ res.status,
6804
+ method,
6805
+ url,
6806
+ (diagnostic) => new PromotionsApiError(res.status, null, diagnostic)
6807
+ );
5847
6808
  if (res.status === 204) return null;
5848
6809
  const body = await res.json().catch(() => null);
5849
6810
  if (res.status >= 400) {
5850
6811
  throw new PromotionsApiError(
5851
6812
  res.status,
5852
6813
  body,
5853
- formatMessage(msg.value.apiErrorHttpStatus, { status: res.status })
6814
+ `Promotions API responded with HTTP ${res.status}`
5854
6815
  );
5855
6816
  }
5856
6817
  return body;
@@ -5872,7 +6833,8 @@ function usePromotions(options) {
5872
6833
  method: "POST",
5873
6834
  body: JSON.stringify(data)
5874
6835
  });
5875
- if (!created) throw new PromotionsApiError(0, null, "Create returned no body");
6836
+ if (!created)
6837
+ throw markEmptyResponse(new PromotionsApiError(0, null, "Create returned no body"));
5876
6838
  promotions.value = [...promotions.value, created];
5877
6839
  return created;
5878
6840
  }
@@ -5881,7 +6843,8 @@ function usePromotions(options) {
5881
6843
  method: "PATCH",
5882
6844
  body: JSON.stringify(data)
5883
6845
  });
5884
- if (!updated) throw new PromotionsApiError(0, null, "Update returned no body");
6846
+ if (!updated)
6847
+ throw markEmptyResponse(new PromotionsApiError(0, null, "Update returned no body"));
5885
6848
  promotions.value = promotions.value.map((p) => p.id === id ? updated : p);
5886
6849
  return updated;
5887
6850
  }
@@ -5896,13 +6859,14 @@ function usePromotions(options) {
5896
6859
  }
5897
6860
 
5898
6861
  // src/vue/use-plans.ts
5899
- var import_vue25 = require("vue");
6862
+ var import_vue29 = require("vue");
5900
6863
  var PlansApiError = class extends Error {
5901
6864
  constructor(status, body, message) {
5902
6865
  super(message);
5903
6866
  this.status = status;
5904
6867
  this.body = body;
5905
6868
  this.name = "PlansApiError";
6869
+ markPlatformError(this);
5906
6870
  }
5907
6871
  status;
5908
6872
  body;
@@ -5917,18 +6881,19 @@ function usePlans(options) {
5917
6881
  throw new Error("usePlans: `projectKey` is required.");
5918
6882
  }
5919
6883
  const http = options.http ?? defaultHttpClient();
5920
- const plans = (0, import_vue25.ref)([]);
5921
- const loading = (0, import_vue25.ref)(false);
5922
- const error = (0, import_vue25.ref)(null);
5923
- const tenantCountsByPlanKey = (0, import_vue25.ref)({});
6884
+ const plans = (0, import_vue29.ref)([]);
6885
+ const loading = (0, import_vue29.ref)(false);
6886
+ const error = (0, import_vue29.ref)(null);
6887
+ const tenantCountsByPlanKey = (0, import_vue29.ref)({});
5924
6888
  const baseUrl = `${options.adminEndpoint}/catalog/plans`;
5925
6889
  function authHeaders() {
5926
6890
  const token = options.getAuthToken?.();
5927
6891
  return token ? { Authorization: `Bearer ${token}` } : {};
5928
6892
  }
5929
6893
  async function fetchJson(url, init) {
6894
+ const method = init?.method ?? "GET";
5930
6895
  const res = await http(url, {
5931
- method: init?.method ?? "GET",
6896
+ method,
5932
6897
  headers: {
5933
6898
  "content-type": "application/json",
5934
6899
  ...authHeaders(),
@@ -5936,6 +6901,12 @@ function usePlans(options) {
5936
6901
  },
5937
6902
  body: init?.body
5938
6903
  });
6904
+ requireServerAnswer(
6905
+ res.status,
6906
+ method,
6907
+ url,
6908
+ (diagnostic) => new PlansApiError(res.status, null, diagnostic)
6909
+ );
5939
6910
  if (res.status === 204) return null;
5940
6911
  const body = await res.json().catch(() => null);
5941
6912
  if (res.status >= 400) {
@@ -5976,7 +6947,8 @@ function usePlans(options) {
5976
6947
  method: "POST",
5977
6948
  body: JSON.stringify(data)
5978
6949
  });
5979
- if (!created) throw new PlansApiError(0, null, "Create returned no body");
6950
+ if (!created)
6951
+ throw markEmptyResponse(new PlansApiError(0, null, "Create returned no body"));
5980
6952
  plans.value = [...plans.value, created];
5981
6953
  return created;
5982
6954
  }
@@ -5985,7 +6957,8 @@ function usePlans(options) {
5985
6957
  method: "PATCH",
5986
6958
  body: JSON.stringify(data)
5987
6959
  });
5988
- if (!updated) throw new PlansApiError(0, null, "Update returned no body");
6960
+ if (!updated)
6961
+ throw markEmptyResponse(new PlansApiError(0, null, "Update returned no body"));
5989
6962
  plans.value = plans.value.map((p) => p.id === planId ? updated : p);
5990
6963
  return updated;
5991
6964
  }
@@ -6021,9 +6994,9 @@ function usePlanVersions(options) {
6021
6994
  throw new Error("usePlanVersions: `planId` is required.");
6022
6995
  }
6023
6996
  const http = options.http ?? defaultHttpClient();
6024
- const versions = (0, import_vue25.ref)([]);
6025
- const loading = (0, import_vue25.ref)(false);
6026
- const error = (0, import_vue25.ref)(null);
6997
+ const versions = (0, import_vue29.ref)([]);
6998
+ const loading = (0, import_vue29.ref)(false);
6999
+ const error = (0, import_vue29.ref)(null);
6027
7000
  const planVersionsUrl = `${options.adminEndpoint}/catalog/plans/${options.planId}/versions`;
6028
7001
  const versionUrlBase = `${options.adminEndpoint}/catalog/plan-versions`;
6029
7002
  function authHeaders() {
@@ -6031,8 +7004,9 @@ function usePlanVersions(options) {
6031
7004
  return token ? { Authorization: `Bearer ${token}` } : {};
6032
7005
  }
6033
7006
  async function fetchJson(url, init) {
7007
+ const method = init?.method ?? "GET";
6034
7008
  const res = await http(url, {
6035
- method: init?.method ?? "GET",
7009
+ method,
6036
7010
  headers: {
6037
7011
  "content-type": "application/json",
6038
7012
  ...authHeaders(),
@@ -6040,6 +7014,12 @@ function usePlanVersions(options) {
6040
7014
  },
6041
7015
  body: init?.body
6042
7016
  });
7017
+ requireServerAnswer(
7018
+ res.status,
7019
+ method,
7020
+ url,
7021
+ (diagnostic) => new PlansApiError(res.status, null, diagnostic)
7022
+ );
6043
7023
  if (res.status === 204) return null;
6044
7024
  const body = await res.json().catch(() => null);
6045
7025
  if (res.status >= 400) {
@@ -6068,7 +7048,8 @@ function usePlanVersions(options) {
6068
7048
  method: "POST",
6069
7049
  body: JSON.stringify(data)
6070
7050
  });
6071
- if (!result) throw new PlansApiError(0, null, "CreateDraft returned no body");
7051
+ if (!result)
7052
+ throw markEmptyResponse(new PlansApiError(0, null, "CreateDraft returned no body"));
6072
7053
  versions.value = [...versions.value, result.planVersion];
6073
7054
  return result;
6074
7055
  }
@@ -6077,7 +7058,8 @@ function usePlanVersions(options) {
6077
7058
  `${versionUrlBase}/${versionId}`,
6078
7059
  { method: "PATCH", body: JSON.stringify(data) }
6079
7060
  );
6080
- if (!result) throw new PlansApiError(0, null, "UpdateDraft returned no body");
7061
+ if (!result)
7062
+ throw markEmptyResponse(new PlansApiError(0, null, "UpdateDraft returned no body"));
6081
7063
  versions.value = versions.value.map((v) => v.id === versionId ? result.planVersion : v);
6082
7064
  return result;
6083
7065
  }
@@ -6086,7 +7068,8 @@ function usePlanVersions(options) {
6086
7068
  `${versionUrlBase}/${versionId}/publish`,
6087
7069
  { method: "POST", body: JSON.stringify(opts) }
6088
7070
  );
6089
- if (!result) throw new PlansApiError(0, null, "Publish returned no body");
7071
+ if (!result)
7072
+ throw markEmptyResponse(new PlansApiError(0, null, "Publish returned no body"));
6090
7073
  versions.value = versions.value.map((v) => v.id === versionId ? result.planVersion : v);
6091
7074
  return result;
6092
7075
  }
@@ -6099,7 +7082,8 @@ function usePlanVersions(options) {
6099
7082
  `${versionUrlBase}/${versionId}/terminate`,
6100
7083
  { method: "POST", body: JSON.stringify({ endsAt }) }
6101
7084
  );
6102
- if (!updated) throw new PlansApiError(0, null, "Terminate returned no body");
7085
+ if (!updated)
7086
+ throw markEmptyResponse(new PlansApiError(0, null, "Terminate returned no body"));
6103
7087
  versions.value = versions.value.map((v) => v.id === versionId ? updated : v);
6104
7088
  return updated;
6105
7089
  }
@@ -6120,7 +7104,7 @@ function usePlanVersions(options) {
6120
7104
  }
6121
7105
 
6122
7106
  // src/vue/use-live-plan-versions.ts
6123
- var import_vue26 = require("vue");
7107
+ var import_vue30 = require("vue");
6124
7108
  function useLivePlanVersions(options) {
6125
7109
  if (!options?.adminEndpoint) {
6126
7110
  throw new Error("useLivePlanVersions: `adminEndpoint` is required.");
@@ -6129,9 +7113,9 @@ function useLivePlanVersions(options) {
6129
7113
  throw new Error("useLivePlanVersions: `plans` is required.");
6130
7114
  }
6131
7115
  const http = options.http ?? defaultHttpClient();
6132
- const livePlanVersions = (0, import_vue26.ref)({});
6133
- const loading = (0, import_vue26.ref)(false);
6134
- const error = (0, import_vue26.ref)(null);
7116
+ const livePlanVersions = (0, import_vue30.ref)({});
7117
+ const loading = (0, import_vue30.ref)(false);
7118
+ const error = (0, import_vue30.ref)(null);
6135
7119
  function authHeaders() {
6136
7120
  const token = options.getAuthToken?.();
6137
7121
  return token ? { Authorization: `Bearer ${token}` } : {};
@@ -6173,7 +7157,7 @@ function useLivePlanVersions(options) {
6173
7157
  loading.value = false;
6174
7158
  }
6175
7159
  }
6176
- (0, import_vue26.watch)(
7160
+ (0, import_vue30.watch)(
6177
7161
  () => options.plans.value.map((p) => p.id).join(","),
6178
7162
  () => {
6179
7163
  if (options.plans.value.length > 0) void refresh();
@@ -6195,12 +7179,12 @@ function findLatestLive(versions) {
6195
7179
  }
6196
7180
 
6197
7181
  // src/vue/use-manifest.ts
6198
- var import_vue27 = require("vue");
7182
+ var import_vue31 = require("vue");
6199
7183
  function useManifest(options) {
6200
7184
  const loader = new ManifestLoader(options);
6201
- const manifest = (0, import_vue27.ref)(null);
6202
- const loading = (0, import_vue27.ref)(false);
6203
- const error = (0, import_vue27.ref)(null);
7185
+ const manifest = (0, import_vue31.ref)(null);
7186
+ const loading = (0, import_vue31.ref)(false);
7187
+ const error = (0, import_vue31.ref)(null);
6204
7188
  async function load() {
6205
7189
  loading.value = true;
6206
7190
  error.value = null;
@@ -6226,24 +7210,24 @@ function useManifest(options) {
6226
7210
  }
6227
7211
 
6228
7212
  // src/vue/use-nav.ts
6229
- var import_vue28 = require("vue");
7213
+ var import_vue32 = require("vue");
6230
7214
  function useNav(manifest, options = {}) {
6231
7215
  const { locale } = useSuperAdminI18n();
6232
- const activeLocale = (0, import_vue28.computed)(() => options.locale ?? locale.value);
6233
- const routes = (0, import_vue28.computed)(() => {
7216
+ const activeLocale = (0, import_vue32.computed)(() => options.locale ?? locale.value);
7217
+ const routes = (0, import_vue32.computed)(() => {
6234
7218
  if (!manifest.value) return [];
6235
7219
  return buildRoutes(manifest.value, { ...options, locale: activeLocale.value });
6236
7220
  });
6237
- const sidebar = (0, import_vue28.computed)(
7221
+ const sidebar = (0, import_vue32.computed)(
6238
7222
  () => buildSidebar(routes.value, defaultSectionOrder(activeLocale.value))
6239
7223
  );
6240
7224
  return { routes, sidebar };
6241
7225
  }
6242
7226
 
6243
7227
  // src/vue/use-actions.ts
6244
- var import_vue29 = require("vue");
7228
+ var import_vue33 = require("vue");
6245
7229
  function useActions(manifest, actions) {
6246
- const registry = (0, import_vue29.computed)(() => {
7230
+ const registry = (0, import_vue33.computed)(() => {
6247
7231
  if (!manifest.value) return null;
6248
7232
  return new ActionRegistry(manifest.value, actions);
6249
7233
  });
@@ -6251,18 +7235,18 @@ function useActions(manifest, actions) {
6251
7235
  }
6252
7236
 
6253
7237
  // src/vue/use-tenant-action-flow.ts
6254
- var import_vue30 = require("vue");
7238
+ var import_vue34 = require("vue");
6255
7239
  function useTenantActionFlow(manifest, providers = {}) {
6256
7240
  const handlers = useSuperAdminActions();
6257
7241
  const msg = useSaMessages("tenants");
6258
- const registry = (0, import_vue30.computed)(() => {
7242
+ const registry = (0, import_vue34.computed)(() => {
6259
7243
  if (!manifest.value) return null;
6260
7244
  return new ActionRegistry(manifest.value, handlers);
6261
7245
  });
6262
- const orphanedDefs = (0, import_vue30.computed)(
7246
+ const orphanedDefs = (0, import_vue34.computed)(
6263
7247
  () => registry.value ? registry.value.listOrphanedDefs() : []
6264
7248
  );
6265
- const availableActions = (0, import_vue30.computed)(() => {
7249
+ const availableActions = (0, import_vue34.computed)(() => {
6266
7250
  const m = manifest.value;
6267
7251
  const reg = registry.value;
6268
7252
  if (!m || !reg) return [];
@@ -6335,7 +7319,7 @@ async function runFlow(def, row, registry, providers, successTemplate) {
6335
7319
  }
6336
7320
 
6337
7321
  // src/vue/use-platform-tenant-actions.ts
6338
- var import_vue31 = require("vue");
7322
+ var import_vue35 = require("vue");
6339
7323
  var DEFAULT_VISIBLE_FOR_ROW = (def, row) => {
6340
7324
  if (def.actionKey === "tenants.suspend") return row.isActive;
6341
7325
  if (def.actionKey === "tenants.reactivate") return !row.isActive;
@@ -6371,7 +7355,7 @@ function usePlatformTenantActions(options) {
6371
7355
  const iconForActionKey = options.iconForActionKey ?? DEFAULT_ICON_FOR_ACTION_KEY;
6372
7356
  const toneForActionKey = options.toneForActionKey ?? DEFAULT_TONE_FOR_ACTION_KEY;
6373
7357
  const visibleForRow = options.visibleForRow ?? DEFAULT_VISIBLE_FOR_ROW;
6374
- const mfa = (0, import_vue31.ref)({ show: false, description: "", error: "" });
7358
+ const mfa = (0, import_vue35.ref)({ show: false, description: "", error: "" });
6375
7359
  let pendingMfaResolve = null;
6376
7360
  function showMfaDialog(def, ctx) {
6377
7361
  return new Promise((resolve) => {
@@ -6396,7 +7380,7 @@ function usePlatformTenantActions(options) {
6396
7380
  pendingMfaResolve(null);
6397
7381
  }
6398
7382
  }
6399
- const confirmDialog = (0, import_vue31.ref)({
7383
+ const confirmDialog = (0, import_vue35.ref)({
6400
7384
  show: false,
6401
7385
  def: null,
6402
7386
  row: null
@@ -6438,7 +7422,7 @@ function usePlatformTenantActions(options) {
6438
7422
  },
6439
7423
  visibleForRow
6440
7424
  });
6441
- const manifestActions = (0, import_vue31.computed)(
7425
+ const manifestActions = (0, import_vue35.computed)(
6442
7426
  () => flow.availableActions.value.map((def) => ({
6443
7427
  id: def.id,
6444
7428
  label: def.label,
@@ -6452,7 +7436,7 @@ function usePlatformTenantActions(options) {
6452
7436
  }
6453
7437
  }))
6454
7438
  );
6455
- const realOrphans = (0, import_vue31.computed)(() => {
7439
+ const realOrphans = (0, import_vue35.computed)(() => {
6456
7440
  const caps = options.manifest.value?.capabilities ?? {};
6457
7441
  const defs = options.manifest.value?.tenants?.actions ?? [];
6458
7442
  const defByActionKey = new Map(defs.map((d) => [d.actionKey, d]));
@@ -6477,12 +7461,12 @@ function usePlatformTenantActions(options) {
6477
7461
  }
6478
7462
 
6479
7463
  // src/vue/use-batch-columns.ts
6480
- var import_vue32 = require("vue");
7464
+ var import_vue36 = require("vue");
6481
7465
  function useBatchColumns(manifest, tenantIds, options = {}) {
6482
7466
  const fetcher = new BatchColumnFetcher(options);
6483
- const data = (0, import_vue32.ref)({});
6484
- const loading = (0, import_vue32.ref)(false);
6485
- const error = (0, import_vue32.ref)(null);
7467
+ const data = (0, import_vue36.ref)({});
7468
+ const loading = (0, import_vue36.ref)(false);
7469
+ const error = (0, import_vue36.ref)(null);
6486
7470
  async function load() {
6487
7471
  if (!manifest.value || tenantIds.value.length === 0) {
6488
7472
  data.value = {};
@@ -6499,22 +7483,23 @@ function useBatchColumns(manifest, tenantIds, options = {}) {
6499
7483
  loading.value = false;
6500
7484
  }
6501
7485
  }
6502
- (0, import_vue32.watch)([manifest, tenantIds], () => {
7486
+ (0, import_vue36.watch)([manifest, tenantIds], () => {
6503
7487
  void load();
6504
7488
  });
6505
7489
  return { data, loading, error, reload: load };
6506
7490
  }
6507
7491
 
6508
7492
  // src/vue/platform-loaders.ts
6509
- function resolveEndpoints(endpoints) {
7493
+ function resolveSuperAdminEndpoints(endpoints) {
6510
7494
  return {
6511
7495
  apiBase: endpoints.apiBase,
6512
7496
  publicBootEndpoint: endpoints.publicBootEndpoint ?? `${endpoints.apiBase}/boot`,
6513
- manifestEndpoint: endpoints.manifestEndpoint ?? `${endpoints.apiBase}/manifest`
7497
+ manifestEndpoint: endpoints.manifestEndpoint ?? `${endpoints.apiBase}/manifest`,
7498
+ projectKey: endpoints.projectKey ?? ""
6514
7499
  };
6515
7500
  }
6516
7501
  function createPlatformLoaders(options) {
6517
- const resolved = resolveEndpoints(options.endpoints);
7502
+ const resolved = resolveSuperAdminEndpoints(options.endpoints);
6518
7503
  const bootLoader = new BootLoader({
6519
7504
  endpoint: resolved.publicBootEndpoint,
6520
7505
  http: options.http
@@ -6531,13 +7516,13 @@ function createPlatformLoaders(options) {
6531
7516
 
6532
7517
  // src/vue/manifest-store-factory.ts
6533
7518
  var import_pinia = require("pinia");
6534
- var import_vue33 = require("vue");
7519
+ var import_vue37 = require("vue");
6535
7520
  function createManifestStore(options) {
6536
7521
  return (0, import_pinia.defineStore)(options.id ?? "admin-manifest", () => {
6537
- const manifest = (0, import_vue33.ref)(null);
6538
- const loading = (0, import_vue33.ref)(false);
6539
- const error = (0, import_vue33.ref)(null);
6540
- const loaded = (0, import_vue33.ref)(false);
7522
+ const manifest = (0, import_vue37.ref)(null);
7523
+ const loading = (0, import_vue37.ref)(false);
7524
+ const error = (0, import_vue37.ref)(null);
7525
+ const loaded = (0, import_vue37.ref)(false);
6541
7526
  let inflight = null;
6542
7527
  async function load() {
6543
7528
  loading.value = true;
@@ -6970,6 +7955,7 @@ function planChangeWizardI18n(i18n) {
6970
7955
  ALL_ROWS,
6971
7956
  ActionDefNotInManifestError,
6972
7957
  ActionRegistry,
7958
+ AdminError,
6973
7959
  BatchColumnDriftError,
6974
7960
  BatchColumnFetcher,
6975
7961
  BootLoadError,
@@ -6990,6 +7976,10 @@ function planChangeWizardI18n(i18n) {
6990
7976
  IDENTITY_ACCENT_VALUES,
6991
7977
  IDENTITY_NEUTRAL,
6992
7978
  IDENTITY_NEUTRAL_VALUE,
7979
+ LIST_FIRST_PAGE,
7980
+ LIST_PAGE_SIZE_DEFAULT,
7981
+ LIST_PAGE_SIZE_MAX,
7982
+ LIST_PAGINATION_PARAMS,
6993
7983
  ManifestLoadError,
6994
7984
  ManifestLoader,
6995
7985
  MarketingProjectionsApiError,
@@ -6999,15 +7989,18 @@ function planChangeWizardI18n(i18n) {
6999
7989
  PlansApiError,
7000
7990
  ProjectPageHost,
7001
7991
  PromotionsApiError,
7992
+ SA_COLOR_SCHEMES,
7002
7993
  SA_INTL_LOCALES,
7003
7994
  SA_LOCALES,
7004
7995
  SA_LOCALE_LABELS,
7005
7996
  SA_LOCALE_STORAGE_KEY,
7006
7997
  SA_MESSAGES,
7007
7998
  SA_THEME_KEY,
7999
+ SA_THEME_STORAGE_KEY,
7008
8000
  SERVER_PAGE_SIZE_OPTIONS,
7009
8001
  SUPER_ADMIN_ACTIONS_KEY,
7010
8002
  SUPER_ADMIN_BRAND_KEY,
8003
+ SUPER_ADMIN_CONFIRM_KEY,
7011
8004
  SUPER_ADMIN_ENDPOINTS_KEY,
7012
8005
  SUPER_ADMIN_EXTENSIONS_KEY,
7013
8006
  SUPER_ADMIN_HTTP_KEY,
@@ -7016,19 +8009,28 @@ function planChangeWizardI18n(i18n) {
7016
8009
  SUPER_ADMIN_MANIFEST_CLEAR_CACHE_KEY,
7017
8010
  SUPER_ADMIN_MANIFEST_KEY,
7018
8011
  SUPER_ADMIN_NOTIFY_KEY,
8012
+ SUPER_ADMIN_RESOURCES_KEY,
7019
8013
  TenantSubscriptionBundlesApiError,
8014
+ adminErrorMessage,
8015
+ auditResource,
8016
+ bindResource,
7020
8017
  buildFeatureRegistry,
7021
8018
  buildFeatureRouterGuard,
7022
8019
  buildNavigationGuard,
7023
8020
  buildQuotaRegistry,
7024
8021
  buildRoutes,
7025
8022
  buildSidebar,
8023
+ clampListPage,
8024
+ clampListPageSize,
7026
8025
  countPlans,
7027
8026
  createAdminResourceClient,
7028
8027
  createAdminRoutes,
8028
+ createAxiosHttpClient,
8029
+ createFetchHttpClient,
7029
8030
  createManifestStore,
7030
8031
  createPlatformLoaders,
7031
8032
  createProjectPageHostRoute,
8033
+ createResourceRegistry,
7032
8034
  createSaCatalog,
7033
8035
  createSaTheme,
7034
8036
  createSuperAdminI18n,
@@ -7038,30 +8040,57 @@ function planChangeWizardI18n(i18n) {
7038
8040
  defaultSectionOrder,
7039
8041
  defaultTenantPlanSectionI18n,
7040
8042
  defaultToneForActionKey,
8043
+ defineListOp,
7041
8044
  defineMessages,
8045
+ defineResource,
8046
+ filterQueryString,
7042
8047
  formatCurrency,
7043
8048
  formatMessage,
7044
8049
  getJson,
7045
8050
  identityAccentAt,
7046
8051
  identityAccentFor,
7047
8052
  identityChipStyle,
8053
+ isAdminError,
8054
+ isAxiosNoResponseError,
7048
8055
  isCurrentlyValid,
8056
+ isEmptyResponse,
7049
8057
  isExpired,
7050
8058
  isFutureScheduled,
8059
+ isPlatformError,
7051
8060
  isProductionBoot,
7052
8061
  isSaBuiltinLocale,
8062
+ isSentInQuery,
8063
+ isTransportFailure,
8064
+ listUrl,
8065
+ markEmptyResponse,
8066
+ markPlatformError,
8067
+ markTransportFailure,
7053
8068
  mergeMessages,
7054
8069
  planChangeWizardI18n,
8070
+ planVersionsResource,
8071
+ plansResource,
8072
+ platformResources,
7055
8073
  postJson,
7056
8074
  provideEntitlement,
8075
+ readErrorCode,
8076
+ readErrorDetail,
8077
+ readListPage,
8078
+ requestJson,
8079
+ requestJsonBody,
8080
+ requireServerAnswer,
7057
8081
  resolveExtension,
7058
8082
  resolveLoginBranding,
7059
8083
  resolveMessages,
7060
8084
  resolvePlans,
8085
+ resolveSuperAdminEndpoints,
8086
+ tenantsResource,
8087
+ toAdminError,
7061
8088
  todayIsoDate,
7062
8089
  trimTrailingSlashes,
7063
8090
  useActions,
7064
8091
  useApiList,
8092
+ useAsyncAction,
8093
+ useAsyncData,
7065
8094
  useAuditEntries,
7066
8095
  useBatchColumns,
7067
8096
  useBulkPublish,
@@ -7083,6 +8112,8 @@ function planChangeWizardI18n(i18n) {
7083
8112
  usePlatformTenantActions,
7084
8113
  usePromotions,
7085
8114
  usePublicBoot,
8115
+ useResource,
8116
+ useResourceList,
7086
8117
  useSaMessages,
7087
8118
  useSaTheme,
7088
8119
  useSubscriptionDraft,