@saasicat/ui-vue 0.25.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +55 -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-COVQLDBZ.js} +759 -24
  5. package/dist/{chunk-TQQMKWN6.js → chunk-DT25QAU2.js} +119 -7
  6. package/dist/{chunk-D74FFCCP.js → chunk-O3Y3BYYS.js} +96 -178
  7. package/dist/client/index.cjs +770 -84
  8. package/dist/client/index.d.cts +429 -9
  9. package/dist/client/index.d.ts +429 -9
  10. package/dist/client/index.js +84 -12
  11. package/dist/index.cjs +1337 -295
  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 +585 -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 +6 -4
  22. package/src/client/admin-error.ts +569 -0
  23. package/src/client/admin-resource-client.ts +9 -12
  24. package/src/client/attach-cause.ts +21 -0
  25. package/src/client/batch-column-fetcher.ts +2 -2
  26. package/src/client/boot-loader.ts +12 -1
  27. package/src/client/http/axios-http-client.ts +383 -0
  28. package/src/client/http/fetch-http-client.ts +89 -0
  29. package/src/client/http/index.ts +6 -0
  30. package/src/client/http-json.ts +82 -16
  31. package/src/client/i18n/messages/discovery.ts +0 -6
  32. package/src/client/i18n/messages/errors.ts +44 -0
  33. package/src/client/i18n/messages/marketing.ts +0 -6
  34. package/src/client/i18n/messages/promos.ts +0 -2
  35. package/src/client/i18n/messages/shell.ts +12 -0
  36. package/src/client/i18n/messages.ts +3 -0
  37. package/src/client/index.ts +6 -0
  38. package/src/client/manifest-loader.ts +65 -15
  39. package/src/client/nav-builder.ts +3 -1
  40. package/src/client/resources/audit.resource.ts +25 -0
  41. package/src/client/resources/define-resource.ts +166 -0
  42. package/src/client/resources/index.ts +42 -0
  43. package/src/client/resources/list-resource.ts +176 -0
  44. package/src/client/resources/plans.resource.ts +169 -0
  45. package/src/client/resources/resource-request.ts +117 -0
  46. package/src/client/resources/tenants.resource.ts +27 -0
  47. package/src/client/types.ts +10 -3
  48. package/src/components/BundleVersionPublishDialog.vue +1 -1
  49. package/src/components/MarketingPromotionsTab.vue +263 -241
  50. package/src/components/ThemeSwitcher.vue +85 -0
  51. package/src/components/bundle-editor/BundleCreatePanel.vue +1 -1
  52. package/src/components/dialogs/PromoCodeDialogFields.vue +20 -20
  53. package/src/components/plan/PlanCycleToggle.vue +1 -1
  54. package/src/components/plan-detail/PlanVersionsPanel.vue +5 -2
  55. package/src/index.ts +5 -0
  56. package/src/pages-standard/AdminLayout.vue +57 -0
  57. package/src/pages-standard/AdminManifestErrorPage.vue +4 -3
  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 +44 -7
  62. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +7 -2
  63. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +44 -6
  64. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +8 -2
  65. package/src/pages-tenant/PackageSnapshotPanel.vue +31 -2
  66. package/src/quasar/confirm.ts +83 -0
  67. package/src/quasar/create-super-admin-app.ts +84 -10
  68. package/src/quasar/dark-bridge.ts +47 -10
  69. package/src/quasar/index.ts +1 -0
  70. package/src/vue/create-admin-routes.ts +9 -1
  71. package/src/vue/platform-loaders.ts +12 -2
  72. package/src/vue/resource-registry.ts +211 -0
  73. package/src/vue/super-admin-context.ts +20 -5
  74. package/src/vue/ui-confirm.ts +76 -0
  75. package/src/vue/use-api-list.ts +50 -45
  76. package/src/vue/use-async-action.ts +170 -0
  77. package/src/vue/use-async-data.ts +83 -0
  78. package/src/vue/use-bundles.ts +34 -7
  79. package/src/vue/use-catalog-entries.ts +37 -12
  80. package/src/vue/use-discovery.ts +27 -6
  81. package/src/vue/use-marketing-projections.ts +29 -7
  82. package/src/vue/use-plans.ts +36 -8
  83. package/src/vue/use-promotions.ts +27 -7
  84. package/src/vue/use-resource-list.ts +272 -0
  85. package/src/vue/use-sa-theme.ts +34 -5
  86. 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,298 @@ 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/attach-cause.ts
210
+ function attachCause(error, cause) {
211
+ Object.defineProperty(error, "cause", {
212
+ value: cause,
213
+ configurable: true,
214
+ writable: true
215
+ });
216
+ return error;
217
+ }
218
+
219
+ // src/client/i18n/format.ts
220
+ var import_types = require("@saasicat/types");
221
+ function formatMessage(template, params) {
222
+ return (0, import_types.formatErrorMessage)(template, params);
223
+ }
224
+
225
+ // src/client/admin-error.ts
226
+ var ADMIN_ERROR = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/AdminError");
227
+ var PLATFORM_ERROR = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/PlatformError");
228
+ function markPlatformError(error) {
229
+ Object.defineProperty(error, PLATFORM_ERROR, { value: true });
230
+ }
231
+ function isPlatformError(value) {
232
+ return typeof value === "object" && value !== null && PLATFORM_ERROR in value;
233
+ }
234
+ var EMPTY_RESPONSE = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/EmptyResponse");
235
+ function markEmptyResponse(error) {
236
+ Object.defineProperty(error, EMPTY_RESPONSE, { value: true });
237
+ return error;
238
+ }
239
+ function isEmptyResponse(value) {
240
+ return typeof value === "object" && value !== null && EMPTY_RESPONSE in value;
241
+ }
242
+ var TRANSPORT_FAILURE = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/TransportFailure");
243
+ function markTransportFailure(error) {
244
+ if (typeof error === "object" && error !== null) {
245
+ try {
246
+ Object.defineProperty(error, TRANSPORT_FAILURE, { value: true });
247
+ } catch {
248
+ }
249
+ }
250
+ return error;
251
+ }
252
+ function isTransportFailure(value) {
253
+ return typeof value === "object" && value !== null && TRANSPORT_FAILURE in value;
254
+ }
255
+ function describe(init) {
256
+ const status = init.status ?? 0;
257
+ const where = [init.method, init.url].filter(Boolean).join(" ");
258
+ const what = status > 0 ? `HTTP ${status}` : "request failed";
259
+ const head = where ? `${where} \u2192 ${what}` : what;
260
+ const code = init.code ? ` (${init.code})` : "";
261
+ return init.detail ? `${head}${code}: ${init.detail}` : `${head}${code}`;
262
+ }
263
+ var AdminError = class extends Error {
264
+ /** HTTP status. `0` means the request never produced one. */
265
+ status;
266
+ /** Machine-readable code from the response body, when it carried one. */
267
+ code;
268
+ /** Parsed response body, when there was one. */
269
+ body;
270
+ /** Requested URL, when known. */
271
+ url;
272
+ /** Request method, when known. */
273
+ method;
274
+ /**
275
+ * Text the failing side supplied — the only candidate for user-facing
276
+ * output. `undefined` when nothing was supplied.
277
+ */
278
+ detail;
279
+ /** The request completed but the answer was unusable. See `AdminErrorInit`. */
280
+ emptyResponse;
281
+ /** The request never reached the server. See `AdminErrorInit`. */
282
+ transportFailure;
283
+ constructor(init = {}) {
284
+ super(init.message ?? describe(init));
285
+ if (init.cause !== void 0) attachCause(this, init.cause);
286
+ this.name = "AdminError";
287
+ this.status = init.status ?? 0;
288
+ this.code = init.code;
289
+ this.body = init.body;
290
+ this.url = init.url;
291
+ this.method = init.method;
292
+ this.detail = init.detail;
293
+ this.emptyResponse = init.emptyResponse ?? false;
294
+ this.transportFailure = init.transportFailure ?? false;
295
+ Object.defineProperty(this, ADMIN_ERROR, { value: true });
296
+ markPlatformError(this);
297
+ }
298
+ };
299
+ function isAdminError(err) {
300
+ return typeof err === "object" && err !== null && ADMIN_ERROR in err;
301
+ }
302
+ function asRecord(value) {
303
+ return typeof value === "object" && value !== null ? value : void 0;
304
+ }
305
+ function asString(value) {
306
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
307
+ }
308
+ function isAxiosNoResponseError(value) {
309
+ const record = asRecord(value);
310
+ if (!record) return false;
311
+ return record.isAxiosError === true && record.request != null && record.response == null;
312
+ }
313
+ function readErrorDetail(body) {
314
+ const record = asRecord(body);
315
+ if (!record) return asString(body);
316
+ const message = record.message;
317
+ if (Array.isArray(message)) {
318
+ const parts = message.filter((part) => typeof part === "string");
319
+ return parts.length > 0 ? parts.join(", ") : void 0;
320
+ }
321
+ return asString(message) ?? asString(record.error) ?? asString(record.reason);
322
+ }
323
+ function readErrorCode(body) {
324
+ return asString(asRecord(body)?.code);
325
+ }
326
+ function toAdminError(err) {
327
+ if (isAdminError(err)) return err;
328
+ const record = asRecord(err);
329
+ const response = asRecord(record?.response);
330
+ if (response && typeof response.status === "number") {
331
+ const config = asRecord(record?.config);
332
+ const body = response.data;
333
+ return new AdminError({
334
+ status: response.status,
335
+ code: readErrorCode(body),
336
+ body,
337
+ url: asString(config?.url),
338
+ method: asString(config?.method)?.toUpperCase(),
339
+ // Only what the response actually said. `err.message` here is
340
+ // axios's own generated line ("Request failed with status code
341
+ // 401") — English, untranslated, and about the transport rather
342
+ // than about what went wrong. Letting it through as `detail` would
343
+ // make `adminErrorMessage` return it in preference to the
344
+ // localized wording, which is the whole point of that wording.
345
+ detail: readErrorDetail(body),
346
+ cause: err
347
+ });
348
+ }
349
+ if (record && typeof record.status === "number") {
350
+ const ours = isPlatformError(err);
351
+ return new AdminError({
352
+ status: record.status,
353
+ code: readErrorCode(record.body) ?? asString(record.code),
354
+ body: record.body,
355
+ // Same reasoning as above: `BundlesApiError`'s message is
356
+ // "Bundles API responded with HTTP 403", a diagnostic. It stays on
357
+ // `message`, where logs read it.
358
+ detail: readErrorDetail(record.body) ?? (ours ? void 0 : asString(record.message)),
359
+ // Asked of the throw site, not of the class: only the site that
360
+ // read the response knows whether one arrived. `status === 0 &&
361
+ // ours` was the same question put to the wrong witness — the
362
+ // loaders raise branded errors from a status the client chose, so
363
+ // a client that resolves a transport failure as `0` turned a
364
+ // failed GET into "the change may have been applied".
365
+ emptyResponse: isEmptyResponse(err),
366
+ transportFailure: isTransportFailure(err),
367
+ message: asString(record.message),
368
+ cause: err
369
+ });
370
+ }
371
+ if (err instanceof Error) {
372
+ const transport = isTransportFailure(err) || isAxiosNoResponseError(err);
373
+ const saidSomethingReadable = !transport && !(err instanceof TypeError);
374
+ return new AdminError({
375
+ detail: saidSomethingReadable ? asString(err.message) : void 0,
376
+ transportFailure: transport,
377
+ message: err.message,
378
+ cause: err
379
+ });
380
+ }
381
+ const text = asString(err) ?? asString(record?.message);
382
+ return new AdminError({
383
+ code: asString(record?.code),
384
+ detail: text,
385
+ message: text,
386
+ cause: err
387
+ });
388
+ }
389
+ function statusKey(status) {
390
+ if (status === 400 || status === 422) return "validation";
391
+ if (status === 401) return "unauthorized";
392
+ if (status === 403) return "forbidden";
393
+ if (status === 404) return "notFound";
394
+ if (status === 409) return "conflict";
395
+ if (status === 429) return "rateLimited";
396
+ if (status >= 500) return "server";
397
+ return void 0;
398
+ }
399
+ function adminErrorMessage(err, msgs) {
400
+ const error = toAdminError(err);
401
+ if (error.detail) return error.detail;
402
+ if (error.emptyResponse) return msgs.emptyResponse;
403
+ if (error.transportFailure) return msgs.network;
404
+ if (error.status > 0) {
405
+ const key = statusKey(error.status);
406
+ return key ? msgs[key] : formatMessage(msgs.httpStatus, { status: error.status });
407
+ }
408
+ return msgs.unexpected;
409
+ }
410
+
411
+ // src/client/http-json.ts
412
+ function serverAnswered(status) {
413
+ return status > 0;
414
+ }
415
+ function requireServerAnswer(status, method, url, raise) {
416
+ if (serverAnswered(status)) return;
417
+ throw markTransportFailure(
418
+ raise(`${method} ${url} produced no HTTP status \u2014 the request did not reach the server`)
419
+ );
420
+ }
421
+ var HttpJsonError = AdminError;
422
+ async function readErrorBody(res) {
423
+ try {
424
+ return await res.json();
425
+ } catch {
426
+ return void 0;
427
+ }
428
+ }
429
+ async function failed(res, method, url) {
430
+ const body = await readErrorBody(res);
431
+ return new AdminError({
432
+ status: res.status,
433
+ // Read here rather than left to the status: `status: 0` reaches
434
+ // `adminErrorMessage` as "nothing is known about this failure", and
435
+ // this seam knows better — it held the response.
436
+ transportFailure: !serverAnswered(res.status),
437
+ code: readErrorCode(body),
438
+ body,
439
+ url,
440
+ method,
441
+ // Shared with `toAdminError` on purpose: a NestJS `ValidationPipe`
442
+ // rejection carries `message` as an array, and reading it here in a
443
+ // second, narrower way is how those constraints went missing.
444
+ detail: readErrorDetail(body)
445
+ });
446
+ }
447
+ function trimTrailingSlashes(url) {
448
+ let end = url.length;
449
+ while (end > 0 && url[end - 1] === "/") end--;
450
+ return url.slice(0, end);
451
+ }
452
+ async function getJson(http, url) {
453
+ const res = await http(url);
454
+ if (res.status < 200 || res.status >= 300) {
455
+ throw await failed(res, "GET", url);
456
+ }
457
+ return await res.json();
458
+ }
459
+ async function postJson(http, url, body) {
460
+ const res = await http(url, {
461
+ method: "POST",
462
+ headers: { "Content-Type": "application/json" },
463
+ body: JSON.stringify(body)
464
+ });
465
+ if (res.status < 200 || res.status >= 300) {
466
+ throw await failed(res, "POST", url);
467
+ }
468
+ return await res.json();
469
+ }
470
+
471
+ // src/client/http/fetch-http-client.ts
472
+ var ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:|^\/\//i;
473
+ function resolveUrl(url, baseUrl) {
474
+ if (!baseUrl || ABSOLUTE_URL.test(url)) return url;
475
+ return `${trimTrailingSlashes(baseUrl)}${url.startsWith("/") ? "" : "/"}${url}`;
476
+ }
477
+ function createFetchHttpClient(options = {}) {
478
+ return async (url, init) => {
479
+ const headers = new Headers(await options.headers?.());
480
+ if (!headers.has("Accept")) headers.set("Accept", "application/json");
481
+ for (const [name, value] of Object.entries(init?.headers ?? {})) {
482
+ headers.set(name, value);
483
+ }
484
+ if (init?.body !== void 0 && !headers.has("Content-Type")) {
485
+ headers.set("Content-Type", "application/json");
486
+ }
487
+ let response;
488
+ try {
489
+ response = await fetch(resolveUrl(url, options.baseUrl), {
490
+ method: init?.method ?? "GET",
491
+ headers,
492
+ body: init?.body
493
+ });
494
+ } catch (err) {
495
+ throw markTransportFailure(err);
496
+ }
497
+ return response;
498
+ };
499
+ }
500
+
163
501
  // src/client/types.ts
164
502
  var NOOP_KV_STORE = {
165
503
  get: () => null,
@@ -201,51 +539,326 @@ function resolveLocalStorage() {
201
539
  }
202
540
  }
203
541
  function defaultHttpClient() {
204
- return (url, init) => fetch(url, init);
542
+ return createFetchHttpClient();
205
543
  }
206
544
 
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";
545
+ // src/client/http/axios-http-client.ts
546
+ function stripped(url, prefixes) {
547
+ for (const prefix of prefixes) {
548
+ const trimmed = trimTrailingSlashes(prefix);
549
+ if (!trimmed || !url.startsWith(trimmed)) continue;
550
+ const rest = url.slice(trimmed.length);
551
+ if (rest === "") return "/";
552
+ if (rest.startsWith("/")) return rest;
553
+ if (rest.startsWith("?") || rest.startsWith("#")) return `/${rest}`;
554
+ }
555
+ return url;
556
+ }
557
+ function headerReader(headers) {
558
+ const record = typeof headers === "object" && headers !== null ? headers : {};
559
+ const lowered = /* @__PURE__ */ new Map();
560
+ for (const [key, value] of Object.entries(record)) lowered.set(key.toLowerCase(), value);
561
+ return (name) => {
562
+ const value = lowered.get(name.toLowerCase());
563
+ return value == null ? null : String(value);
564
+ };
565
+ }
566
+ function bodyIsRaw(response, declared) {
567
+ if (declared !== "auto") return declared === "raw";
568
+ if (response.data === "") return true;
569
+ const config = response.config;
570
+ if (config?.transformResponse === null) return true;
571
+ if (Array.isArray(config?.transformResponse) && config.transformResponse.length === 0) {
572
+ return true;
214
573
  }
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;
574
+ const responseType = config?.responseType;
575
+ if (responseType === "json") return false;
576
+ if (typeof responseType === "string" && responseType !== "") return true;
577
+ const transitional = config?.transitional;
578
+ return typeof transitional === "object" && transitional !== null && transitional.forcedJSONParsing === false;
579
+ }
580
+ async function readBody(data) {
581
+ if (typeof data === "string" || data === null || data === void 0) return data;
582
+ if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
583
+ if (ArrayBuffer.isView(data)) return new TextDecoder().decode(data);
584
+ const maybe = data;
585
+ if (typeof maybe.pipe === "function" || typeof maybe.getReader === "function") {
586
+ throw new TypeError(
587
+ "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."
588
+ );
224
589
  }
590
+ if (typeof maybe.text === "function") return maybe.text.call(data);
591
+ return data;
225
592
  }
226
- function trimTrailingSlashes(url) {
227
- let end = url.length;
228
- while (end > 0 && url[end - 1] === "/") end--;
229
- return url.slice(0, end);
593
+ function adapt(response, declared) {
594
+ return {
595
+ status: response.status,
596
+ headers: { get: headerReader(response.headers) },
597
+ json: async () => {
598
+ const data = await readBody(response.data);
599
+ if (typeof data !== "string") return data;
600
+ return bodyIsRaw({ ...response, data }, declared) ? JSON.parse(data) : data;
601
+ },
602
+ text: async () => {
603
+ const data = await readBody(response.data);
604
+ return typeof data === "string" ? data : JSON.stringify(data);
605
+ }
606
+ };
230
607
  }
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));
608
+ function createAxiosHttpClient(instance, options = {}) {
609
+ const prefixes = typeof options.stripPrefix === "string" ? [options.stripPrefix] : options.stripPrefix ?? [];
610
+ const responseBody = options.responseBody ?? "auto";
611
+ return async (url, init) => {
612
+ try {
613
+ return adapt(
614
+ await instance.request({
615
+ url: stripped(url, prefixes),
616
+ method: (init?.method ?? "GET").toUpperCase(),
617
+ headers: init?.headers,
618
+ data: init?.body
619
+ }),
620
+ responseBody
621
+ );
622
+ } catch (err) {
623
+ const response = err?.response;
624
+ if (response && typeof response.status === "number") {
625
+ return adapt(response, responseBody);
626
+ }
627
+ throw isAxiosNoResponseError(err) ? markTransportFailure(err) : err;
628
+ }
629
+ };
630
+ }
631
+
632
+ // src/client/resources/define-resource.ts
633
+ function defineResource(key, ops, options = {}) {
634
+ return { key, ops, projectScoped: options.projectScoped ?? false };
635
+ }
636
+ function requireProjectKey(def, ctx) {
637
+ if (!def.projectScoped) return;
638
+ if (typeof ctx.projectKey === "string" && ctx.projectKey.trim() !== "") return;
639
+ throw new Error(
640
+ `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().`
641
+ );
642
+ }
643
+ function bindResource(def, http, ctx) {
644
+ const readContext = typeof ctx === "function" ? ctx : () => ctx;
645
+ if (def.projectScoped) requireProjectKey(def, readContext());
646
+ const bound = {};
647
+ for (const [name, op] of Object.entries(def.ops)) {
648
+ bound[name] = async (...args) => {
649
+ const context = readContext();
650
+ requireProjectKey(def, context);
651
+ return op(http, context, ...args);
652
+ };
235
653
  }
236
- return await res.json();
654
+ return bound;
237
655
  }
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)
656
+
657
+ // src/client/resources/resource-request.ts
658
+ async function requestJson(http, url, init = {}) {
659
+ const method = init.method ?? "GET";
660
+ const response = await http(url, {
661
+ method,
662
+ headers: { "content-type": "application/json", ...init.headers },
663
+ body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
243
664
  });
244
- if (res.status < 200 || res.status >= 300) {
245
- throw new HttpJsonError(res.status, await extractCode(res));
665
+ requireServerAnswer(
666
+ response.status,
667
+ method,
668
+ url,
669
+ (diagnostic) => new AdminError({ status: response.status, url, method, message: diagnostic })
670
+ );
671
+ if (response.status === 204) return null;
672
+ const body = await response.json().catch(() => null);
673
+ if (response.status >= 400) {
674
+ throw new AdminError({
675
+ status: response.status,
676
+ code: readErrorCode(body),
677
+ body,
678
+ // The third place this had to be read, and the second time it was
679
+ // read a narrower way: without `detail` a page migrating onto these
680
+ // resources loses "Plan already exists" and shows the generic
681
+ // status sentence instead. One decision, one function.
682
+ detail: readErrorDetail(body),
683
+ url,
684
+ method
685
+ });
246
686
  }
247
- return await res.json();
687
+ return body;
688
+ }
689
+ async function requestJsonBody(http, url, what, init = {}) {
690
+ const body = await requestJson(http, url, init);
691
+ if (body === null || body === void 0) {
692
+ throw new AdminError({
693
+ status: 0,
694
+ url,
695
+ method: init.method ?? "GET",
696
+ // `what` is a diagnostic — "Create returned no body" — so it goes
697
+ // on `message`, not `detail`. As `detail` it out-ranked the
698
+ // localized wording and the operator read an English internal
699
+ // string instead of being told the change may already have landed.
700
+ message: what,
701
+ emptyResponse: true
702
+ });
703
+ }
704
+ return body;
705
+ }
706
+
707
+ // src/client/resources/list-resource.ts
708
+ var LIST_FIRST_PAGE = 1;
709
+ var LIST_PAGE_SIZE_DEFAULT = 50;
710
+ var LIST_PAGE_SIZE_MAX = 200;
711
+ var LIST_PAGINATION_PARAMS = ["page", "pageSize"];
712
+ function isSentInQuery(value) {
713
+ return value !== void 0 && value !== null && value !== "";
714
+ }
715
+ function appendFilter(params, filter) {
716
+ for (const [key, value] of Object.entries(filter ?? {})) {
717
+ if (!isSentInQuery(value)) continue;
718
+ params.set(key, String(value));
719
+ }
720
+ }
721
+ function filterQueryString(filter) {
722
+ const params = new URLSearchParams();
723
+ appendFilter(params, filter);
724
+ const query = params.toString();
725
+ return query ? `?${query}` : "";
726
+ }
727
+ function listUrl(endpoint, query = {}) {
728
+ const params = new URLSearchParams();
729
+ params.set("page", String(query.page ?? LIST_FIRST_PAGE));
730
+ params.set("pageSize", String(query.pageSize ?? LIST_PAGE_SIZE_DEFAULT));
731
+ appendFilter(params, query.filter);
732
+ return `${endpoint}${endpoint.includes("?") ? "&" : "?"}${params.toString()}`;
733
+ }
734
+ function readListPage(raw) {
735
+ if (Array.isArray(raw)) return { items: raw, total: raw.length };
736
+ if (raw === null || typeof raw !== "object") return { items: [], total: 0 };
737
+ const body = raw;
738
+ const page = {
739
+ items: Array.isArray(body.items) ? body.items : []
740
+ };
741
+ if (typeof body.total === "number") page.total = body.total;
742
+ if (typeof body.page === "number") page.page = body.page;
743
+ if (typeof body.pageSize === "number") page.pageSize = body.pageSize;
744
+ return page;
248
745
  }
746
+ function clampListPage(page) {
747
+ return Math.max(LIST_FIRST_PAGE, Math.floor(page));
748
+ }
749
+ function clampListPageSize(size) {
750
+ return Math.min(LIST_PAGE_SIZE_MAX, Math.max(1, Math.floor(size)));
751
+ }
752
+ function defineListOp(endpoint) {
753
+ return async (http, ctx, query) => readListPage(await requestJson(http, listUrl(endpoint(ctx), query)));
754
+ }
755
+
756
+ // src/client/resources/plans.resource.ts
757
+ function plansUrl(ctx) {
758
+ return `${ctx.apiBase}/catalog/plans`;
759
+ }
760
+ function versionsUrl(ctx) {
761
+ return `${ctx.apiBase}/catalog/plan-versions`;
762
+ }
763
+ function scoped(url, ctx) {
764
+ return `${url}?projectKey=${encodeURIComponent(ctx.projectKey)}`;
765
+ }
766
+ var plansResource = defineResource(
767
+ "plans",
768
+ {
769
+ list: async (http, ctx) => await requestJson(http, scoped(plansUrl(ctx), ctx)) ?? [],
770
+ tenantCounts: async (http, ctx) => await requestJson(
771
+ http,
772
+ scoped(`${plansUrl(ctx)}/tenant-counts`, ctx)
773
+ ) ?? {},
774
+ /**
775
+ * The project comes from the context, not from the caller.
776
+ *
777
+ * The endpoint takes it in the body, and the composable this replaces
778
+ * made that the caller's job — so a page had to keep hold of the
779
+ * project it was administering just to create in it, and `list()` and
780
+ * `create()` read two different sources for one value. A stale one
781
+ * creates a plan outside the project the list is showing.
782
+ */
783
+ create: (http, ctx, data) => requestJsonBody(http, plansUrl(ctx), "Create returned no body", {
784
+ method: "POST",
785
+ body: { ...data, projectKey: ctx.projectKey }
786
+ }),
787
+ update: (http, ctx, planId, data) => requestJsonBody(
788
+ http,
789
+ `${plansUrl(ctx)}/${planId}`,
790
+ "Update returned no body",
791
+ {
792
+ method: "PATCH",
793
+ body: data
794
+ }
795
+ ),
796
+ /** Marks the plan deleted. Fails while it still has versions. */
797
+ softDelete: async (http, ctx, planId) => {
798
+ await requestJson(http, `${plansUrl(ctx)}/${planId}`, { method: "DELETE" });
799
+ },
800
+ /** Removes the plan for good. 422 `PLAN_HAS_VERSIONS` when it cannot. */
801
+ hardDelete: async (http, ctx, planId) => {
802
+ await requestJson(http, `${plansUrl(ctx)}/${planId}/purge`, { method: "DELETE" });
803
+ }
804
+ },
805
+ { projectScoped: true }
806
+ );
807
+ var planVersionsResource = defineResource("planVersions", {
808
+ listForPlan: async (http, ctx, planId) => await requestJson(http, `${plansUrl(ctx)}/${planId}/versions`) ?? [],
809
+ createDraft: (http, ctx, planId, data) => requestJsonBody(
810
+ http,
811
+ `${plansUrl(ctx)}/${planId}/versions`,
812
+ "CreateDraft returned no body",
813
+ { method: "POST", body: data }
814
+ ),
815
+ updateDraft: (http, ctx, versionId, data) => requestJsonBody(
816
+ http,
817
+ `${versionsUrl(ctx)}/${versionId}`,
818
+ "UpdateDraft returned no body",
819
+ { method: "PATCH", body: data }
820
+ ),
821
+ publish: (http, ctx, versionId, options = {}) => requestJsonBody(
822
+ http,
823
+ `${versionsUrl(ctx)}/${versionId}/publish`,
824
+ "Publish returned no body",
825
+ { method: "POST", body: options }
826
+ ),
827
+ discardDraft: async (http, ctx, versionId) => {
828
+ await requestJson(http, `${versionsUrl(ctx)}/${versionId}`, { method: "DELETE" });
829
+ },
830
+ /**
831
+ * Ends a live version on `endsAt`, with no successor. Idempotent — a
832
+ * second call with another date overwrites the first.
833
+ *
834
+ * `endsAt` is the date itself, not a payload: the endpoint's body is
835
+ * `{ endsAt }` and wrapping it is this operation's job, not the caller's.
836
+ */
837
+ terminate: (http, ctx, versionId, endsAt) => requestJsonBody(
838
+ http,
839
+ `${versionsUrl(ctx)}/${versionId}/terminate`,
840
+ "Terminate returned no body",
841
+ { method: "POST", body: { endsAt } }
842
+ )
843
+ });
844
+
845
+ // src/client/resources/tenants.resource.ts
846
+ var tenantsResource = defineResource("tenants", {
847
+ list: defineListOp((ctx) => `${ctx.apiBase}/tenants`)
848
+ });
849
+
850
+ // src/client/resources/audit.resource.ts
851
+ var auditResource = defineResource("audit", {
852
+ list: defineListOp((ctx) => `${ctx.apiBase}/audit`)
853
+ });
854
+
855
+ // src/client/resources/index.ts
856
+ var platformResources = {
857
+ plans: plansResource,
858
+ planVersions: planVersionsResource,
859
+ tenants: tenantsResource,
860
+ audit: auditResource
861
+ };
249
862
 
250
863
  // src/client/boot-loader.ts
251
864
  var BootLoadError = class extends Error {
@@ -253,6 +866,7 @@ var BootLoadError = class extends Error {
253
866
  super(message);
254
867
  this.status = status;
255
868
  this.name = "BootLoadError";
869
+ markPlatformError(this);
256
870
  }
257
871
  status;
258
872
  };
@@ -270,19 +884,27 @@ var BootLoader = class {
270
884
  }
271
885
  async load() {
272
886
  const res = await this.http(this.endpoint);
887
+ requireServerAnswer(
888
+ res.status,
889
+ "GET",
890
+ this.endpoint,
891
+ (diagnostic) => new BootLoadError(res.status, diagnostic)
892
+ );
273
893
  if (res.status !== 200) {
274
- throw new BootLoadError(res.status, `Boot-Endpunkt antwortete HTTP ${res.status}`);
894
+ throw new BootLoadError(res.status, `Boot endpoint responded with HTTP ${res.status}`);
275
895
  }
276
896
  return await res.json();
277
897
  }
278
898
  };
279
899
 
280
900
  // src/client/manifest-loader.ts
901
+ var NOT_MODIFIED = 304;
281
902
  var ManifestLoadError = class extends Error {
282
903
  constructor(status, message) {
283
904
  super(message);
284
905
  this.status = status;
285
906
  this.name = "ManifestLoadError";
907
+ markPlatformError(this);
286
908
  }
287
909
  status;
288
910
  };
@@ -310,24 +932,48 @@ var ManifestLoader = class {
310
932
  /**
311
933
  * Loads the current manifest. On a cache hit (304) the cached
312
934
  * body is returned — otherwise the fresh server body.
935
+ *
936
+ * A 304 with no usable cached body is not a failure to report but one to
937
+ * undo: the request only carried `If-None-Match` because this loader put
938
+ * the ETag there, and the cache it validated against is this loader's own.
939
+ * So it drops the pair and asks once more without the header, which is the
940
+ * same recovery a person would have had to trigger by hand — except that
941
+ * the person reading an admin screen has neither this loader nor a
942
+ * console. Exactly one retry: a 304 to a request that asked nothing
943
+ * conditional is a server fault, and no repetition fixes it.
313
944
  */
314
945
  async load() {
315
946
  const cachedEtag = this.storage.get(this.etagKey);
947
+ const res = await this.request(cachedEtag);
948
+ if (res.status !== NOT_MODIFIED) return this.acceptFresh(res);
949
+ const cached = this.readCachedBody();
950
+ if (cached) return cached.body;
951
+ if (cachedEtag) {
952
+ this.clearCache();
953
+ const fresh = await this.request(null);
954
+ if (fresh.status !== NOT_MODIFIED) return this.acceptFresh(fresh);
955
+ }
956
+ throw new ManifestLoadError(
957
+ NOT_MODIFIED,
958
+ "Manifest endpoint answered HTTP 304 to a request without If-None-Match"
959
+ );
960
+ }
961
+ /** Sends the manifest GET, conditional when an ETag is supplied. */
962
+ request(etag) {
316
963
  const headers = {};
317
964
  const token = this.getAuthToken?.();
318
965
  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
- }
966
+ if (etag) headers["If-None-Match"] = etag;
967
+ return this.http(this.endpoint, { method: "GET", headers });
968
+ }
969
+ /** Takes a non-304 response as the current manifest and caches it. */
970
+ async acceptFresh(res) {
971
+ requireServerAnswer(
972
+ res.status,
973
+ "GET",
974
+ this.endpoint,
975
+ (diagnostic) => new ManifestLoadError(res.status, diagnostic)
976
+ );
331
977
  if (res.status !== 200) {
332
978
  throw new ManifestLoadError(
333
979
  res.status,
@@ -463,7 +1109,7 @@ var DEFAULT_STANDARD_PAGE_ROUTES = {
463
1109
  platformEmailHistory: "/admin/platform-email-history"
464
1110
  };
465
1111
  function isSupportedStandardPageKey(key) {
466
- return Object.hasOwn(DEFAULT_STANDARD_PAGE_ROUTES, key);
1112
+ return Object.prototype.hasOwnProperty.call(DEFAULT_STANDARD_PAGE_ROUTES, key);
467
1113
  }
468
1114
  var DEFAULT_ICONS = {
469
1115
  dashboard: "dashboard",
@@ -662,7 +1308,7 @@ var ActionRegistry = class {
662
1308
  // src/client/batch-column-fetcher.ts
663
1309
  var BatchColumnDriftError = class extends Error {
664
1310
  constructor(column, reason) {
665
- super(`Spalte "${column.key}": ${reason}`);
1311
+ super(`Column "${column.key}": ${reason}`);
666
1312
  this.column = column;
667
1313
  this.name = "BatchColumnDriftError";
668
1314
  }
@@ -705,7 +1351,7 @@ var BatchColumnFetcher = class {
705
1351
  const res = await this.http(url, { method: "GET", headers });
706
1352
  if (res.status !== 200) {
707
1353
  throw new Error(
708
- `Spalte "${column.key}" \u2014 Endpoint ${column.endpoint} antwortete HTTP ${res.status}`
1354
+ `Column "${column.key}" \u2014 endpoint ${column.endpoint} responded with HTTP ${res.status}`
709
1355
  );
710
1356
  }
711
1357
  const body = await res.json();
@@ -781,10 +1427,10 @@ function createAdminResourceClient(options) {
781
1427
  tenantsEndpoint,
782
1428
  loadTenantDetail: (slug) => getJson2(`${tenantsEndpoint}/${encodeURIComponent(slug)}`),
783
1429
  loadUsers: (filter) => getJson2(
784
- `${base}/users${queryString({ q: filter.q, tenant: filter.tenant })}`
1430
+ `${base}/users${filterQueryString({ q: filter.q, tenant: filter.tenant })}`
785
1431
  ),
786
1432
  loadAudit: (filter) => getJson2(
787
- `${base}/audit${queryString({
1433
+ `${base}/audit${filterQueryString({
788
1434
  actor: filter.actor,
789
1435
  action: filter.action,
790
1436
  entity: filter.entity,
@@ -794,7 +1440,7 @@ function createAdminResourceClient(options) {
794
1440
  ),
795
1441
  loadSubscriptions: () => getJson2(`${base}/subscriptions`),
796
1442
  loadPromos: (filter) => getJson2(
797
- `${base}/promo-codes${queryString({
1443
+ `${base}/promo-codes${filterQueryString({
798
1444
  search: filter.search,
799
1445
  status: filter.status
800
1446
  })}`
@@ -819,21 +1465,6 @@ function assertSuccess(status, method, url) {
819
1465
  throw new Error(`${method} ${url} \u2192 HTTP ${status}`);
820
1466
  }
821
1467
  }
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
1468
 
838
1469
  // src/client/i18n/currency.ts
839
1470
  var DEFAULT_CURRENCY = "EUR";
@@ -1532,9 +2163,6 @@ var discoveryMessages = defineMessages(
1532
2163
  replacedBy: "ersetzt durch {key}",
1533
2164
  replaces: "ersetzt: {keys}",
1534
2165
  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
2166
  capList: {
1539
2167
  orphanFeature: "Feature im Katalog ohne implementierende Capability \u2014 blockiert im blocking-Strict-Mode das Plan-Publish.",
1540
2168
  removedFromCode: "aus Code entfernt"
@@ -1636,9 +2264,6 @@ var discoveryMessages = defineMessages(
1636
2264
  replacedBy: "replaced by {key}",
1637
2265
  replaces: "replaces: {keys}",
1638
2266
  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
2267
  capList: {
1643
2268
  orphanFeature: "Feature in the catalog without an implementing capability \u2014 blocks plan publishing in blocking strict mode.",
1644
2269
  removedFromCode: "removed from code"
@@ -1843,6 +2468,36 @@ var emailMessages = defineMessages(
1843
2468
  }
1844
2469
  );
1845
2470
 
2471
+ // src/client/i18n/messages/errors.ts
2472
+ var errorsMessages = defineMessages(
2473
+ {
2474
+ network: "Die Anfrage konnte nicht abgeschlossen werden. Bitte Verbindung pr\xFCfen.",
2475
+ emptyResponse: "Der Server hat nichts zur\xFCckgegeben. Bitte pr\xFCfen, ob die \xC4nderung angekommen ist.",
2476
+ unauthorized: "Die Sitzung ist abgelaufen. Bitte erneut anmelden.",
2477
+ forbidden: "F\xFCr diese Aktion fehlt die Berechtigung.",
2478
+ notFound: "Der angeforderte Eintrag existiert nicht mehr.",
2479
+ conflict: "Der Eintrag wurde zwischenzeitlich ge\xE4ndert. Bitte neu laden.",
2480
+ validation: "Die Eingaben wurden nicht akzeptiert.",
2481
+ rateLimited: "Zu viele Anfragen. Bitte kurz warten und erneut versuchen.",
2482
+ server: "Der Server konnte die Anfrage nicht verarbeiten.",
2483
+ httpStatus: "Die Anfrage ist fehlgeschlagen (HTTP {status}).",
2484
+ unexpected: "Es ist ein unerwarteter Fehler aufgetreten. Bitte erneut versuchen."
2485
+ },
2486
+ {
2487
+ network: "The request could not be completed. Please check your connection.",
2488
+ emptyResponse: "The server returned nothing. Please check whether the change was applied.",
2489
+ unauthorized: "Your session has expired. Please sign in again.",
2490
+ forbidden: "You do not have permission for this action.",
2491
+ notFound: "The requested entry no longer exists.",
2492
+ conflict: "The entry changed in the meantime. Please reload.",
2493
+ validation: "The input was not accepted.",
2494
+ rateLimited: "Too many requests. Please wait a moment and try again.",
2495
+ server: "The server could not process the request.",
2496
+ httpStatus: "The request failed (HTTP {status}).",
2497
+ unexpected: "Something went wrong. Please try again."
2498
+ }
2499
+ );
2500
+
1846
2501
  // src/client/i18n/messages/marketing.ts
1847
2502
  var marketingMessages = defineMessages(
1848
2503
  {
@@ -1967,9 +2622,6 @@ var marketingMessages = defineMessages(
1967
2622
  colorLabel: "Farbe",
1968
2623
  delete: "Aktion l\xF6schen",
1969
2624
  deleteConfirm: "Aktion \u201E{label}\u201C wirklich l\xF6schen?"
1970
- },
1971
- errors: {
1972
- projectionsApi: "MarketingProjections-API antwortete mit HTTP {status}"
1973
2625
  }
1974
2626
  },
1975
2627
  {
@@ -2094,9 +2746,6 @@ var marketingMessages = defineMessages(
2094
2746
  colorLabel: "Color",
2095
2747
  delete: "Delete promotion",
2096
2748
  deleteConfirm: 'Really delete promotion "{label}"?'
2097
- },
2098
- errors: {
2099
- projectionsApi: "MarketingProjections API responded with HTTP {status}"
2100
2749
  }
2101
2750
  }
2102
2751
  );
@@ -3290,7 +3939,6 @@ var promosMessages = defineMessages(
3290
3939
  {
3291
3940
  title: "Promo-Codes",
3292
3941
  createAction: "Promo-Code anlegen",
3293
- apiErrorHttpStatus: "Promotions-API antwortete mit HTTP {status}",
3294
3942
  list: {
3295
3943
  searchLabel: "Suche (Code)",
3296
3944
  tileScheduled: "Geplant",
@@ -3388,7 +4036,6 @@ var promosMessages = defineMessages(
3388
4036
  {
3389
4037
  title: "Promo codes",
3390
4038
  createAction: "Create promo code",
3391
- apiErrorHttpStatus: "Promotions API responded with HTTP {status}",
3392
4039
  list: {
3393
4040
  searchLabel: "Search (code)",
3394
4041
  tileScheduled: "Scheduled",
@@ -3496,7 +4143,13 @@ var shellMessages = defineMessages(
3496
4143
  subtitle: "SuperAdmin \xB7 Plattform-Verwaltung",
3497
4144
  roleBadge: "SUPER ADMIN",
3498
4145
  logout: "Abmelden",
3499
- language: "Sprache"
4146
+ language: "Sprache",
4147
+ theme: {
4148
+ label: "Darstellung",
4149
+ light: "Hell",
4150
+ dark: "Dunkel",
4151
+ system: "System"
4152
+ }
3500
4153
  },
3501
4154
  drawer: {
3502
4155
  docs: "Doku \xF6ffnen"
@@ -3565,7 +4218,13 @@ var shellMessages = defineMessages(
3565
4218
  subtitle: "SuperAdmin \xB7 Platform administration",
3566
4219
  roleBadge: "SUPER ADMIN",
3567
4220
  logout: "Sign out",
3568
- language: "Language"
4221
+ language: "Language",
4222
+ theme: {
4223
+ label: "Appearance",
4224
+ light: "Light",
4225
+ dark: "Dark",
4226
+ system: "System"
4227
+ }
3569
4228
  },
3570
4229
  drawer: {
3571
4230
  docs: "Open documentation"
@@ -3809,6 +4468,7 @@ var usersMessages = defineMessages(
3809
4468
  function catalogFor(locale) {
3810
4469
  return {
3811
4470
  common: commonMessages[locale],
4471
+ errors: errorsMessages[locale],
3812
4472
  nav: navMessages[locale],
3813
4473
  shell: shellMessages[locale],
3814
4474
  dashboard: dashboardMessages[locale],
@@ -4063,6 +4723,11 @@ var SUPER_ADMIN_NOTIFY_KEY = /* @__PURE__ */ Symbol.for(
4063
4723
  "@saasicat/ui-vue/SUPER_ADMIN_NOTIFY"
4064
4724
  );
4065
4725
 
4726
+ // src/vue/ui-confirm.ts
4727
+ var SUPER_ADMIN_CONFIRM_KEY = /* @__PURE__ */ Symbol.for(
4728
+ "@saasicat/ui-vue/SUPER_ADMIN_CONFIRM"
4729
+ );
4730
+
4066
4731
  // src/vue/use-super-admin-context.ts
4067
4732
  var import_vue = require("vue");
4068
4733
  function useSuperAdminExtensions() {
@@ -4153,10 +4818,10 @@ function useSaMessages(namespace) {
4153
4818
 
4154
4819
  // src/vue/use-sa-theme.ts
4155
4820
  var import_vue3 = require("vue");
4156
- var SCHEMES = ["light", "dark", "system"];
4157
- var STORAGE_KEY = "saasicat.theme.scheme";
4821
+ var SA_COLOR_SCHEMES = ["light", "dark", "system"];
4822
+ var SA_THEME_STORAGE_KEY = "saasicat.theme.scheme";
4158
4823
  function isScheme(value) {
4159
- return typeof value === "string" && SCHEMES.includes(value);
4824
+ return typeof value === "string" && SA_COLOR_SCHEMES.includes(value);
4160
4825
  }
4161
4826
  function systemScheme() {
4162
4827
  const value = (0, import_vue3.ref)("light");
@@ -4177,13 +4842,19 @@ function createSaTheme(options = {}) {
4177
4842
  const persist = options.persist !== false;
4178
4843
  const provided = options.scheme;
4179
4844
  const appOwnsIt = (0, import_vue3.isRef)(provided);
4180
- const stored = persist && !appOwnsIt ? storage.get(STORAGE_KEY) : null;
4845
+ const key = `${options.storageKeyPrefix ?? ""}${SA_THEME_STORAGE_KEY}`;
4846
+ const stored = persist && !appOwnsIt ? storage.get(key) : null;
4181
4847
  const scheme = (0, import_vue3.isRef)(provided) ? provided : (0, import_vue3.ref)(isScheme(stored) ? stored : provided ?? "system");
4182
4848
  const { value: system, unsubscribe } = systemScheme();
4183
- const stopPersisting = persist && !appOwnsIt ? (0, import_vue3.watch)(scheme, (next) => storage.set(STORAGE_KEY, next)) : null;
4849
+ const stopPersisting = persist && !appOwnsIt ? (0, import_vue3.watch)(scheme, (next) => storage.set(key, next)) : null;
4184
4850
  return {
4185
4851
  scheme,
4186
4852
  resolved: (0, import_vue3.computed)(() => scheme.value === "system" ? system.value : scheme.value),
4853
+ // A readonly ref (typically a `computed` derived from a profile
4854
+ // setting) silently swallows writes, so the switcher would be a dead
4855
+ // control. TypeScript ignores `readonly` when checking assignability,
4856
+ // so the guard has to be a runtime one — same as the locale next door.
4857
+ switcherEnabled: (options.switcher ?? true) && !(0, import_vue3.isReadonly)(scheme),
4187
4858
  dispose: () => {
4188
4859
  unsubscribe();
4189
4860
  stopPersisting?.();
@@ -4295,22 +4966,17 @@ var import_vue5 = require("vue");
4295
4966
  function useApiList(options) {
4296
4967
  const http = options.http ?? defaultHttpClient();
4297
4968
  const items = (0, import_vue5.ref)([]);
4298
- const page = (0, import_vue5.ref)(1);
4299
- const pageSize = (0, import_vue5.ref)(50);
4969
+ const page = (0, import_vue5.ref)(LIST_FIRST_PAGE);
4970
+ const pageSize = (0, import_vue5.ref)(LIST_PAGE_SIZE_DEFAULT);
4300
4971
  const total = (0, import_vue5.ref)(0);
4301
4972
  const loading = (0, import_vue5.ref)(false);
4302
4973
  const error = (0, import_vue5.ref)(null);
4303
4974
  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()}`;
4975
+ return listUrl(options.endpoint, {
4976
+ page: page.value,
4977
+ pageSize: pageSize.value,
4978
+ filter: options.filter?.value
4979
+ });
4314
4980
  }
4315
4981
  async function load() {
4316
4982
  loading.value = true;
@@ -4323,20 +4989,11 @@ function useApiList(options) {
4323
4989
  if (res.status !== 200) {
4324
4990
  throw new Error(`Endpoint ${options.endpoint} \u2192 HTTP ${res.status}`);
4325
4991
  }
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
- }
4992
+ const body = readListPage(await res.json());
4993
+ items.value = body.items;
4994
+ if (typeof body.page === "number") page.value = body.page;
4995
+ if (typeof body.pageSize === "number") pageSize.value = body.pageSize;
4996
+ if (typeof body.total === "number") total.value = body.total;
4340
4997
  } catch (err) {
4341
4998
  error.value = err instanceof Error ? err : new Error(String(err));
4342
4999
  items.value = [];
@@ -4346,19 +5003,19 @@ function useApiList(options) {
4346
5003
  }
4347
5004
  }
4348
5005
  async function goToPage(p) {
4349
- page.value = Math.max(1, Math.floor(p));
5006
+ page.value = clampListPage(p);
4350
5007
  await load();
4351
5008
  }
4352
5009
  async function setPageSize(size) {
4353
- pageSize.value = Math.min(200, Math.max(1, Math.floor(size)));
4354
- page.value = 1;
5010
+ pageSize.value = clampListPageSize(size);
5011
+ page.value = LIST_FIRST_PAGE;
4355
5012
  await load();
4356
5013
  }
4357
5014
  if (options.filter) {
4358
5015
  (0, import_vue5.watch)(
4359
5016
  options.filter,
4360
5017
  () => {
4361
- page.value = 1;
5018
+ page.value = LIST_FIRST_PAGE;
4362
5019
  void load();
4363
5020
  },
4364
5021
  { deep: true }
@@ -4404,15 +5061,253 @@ function usePagination(rows, initialRowsPerPage = DEFAULT_ROWS_PER_PAGE) {
4404
5061
  return { page, rowsPerPage, total, pagedRows };
4405
5062
  }
4406
5063
 
4407
- // src/vue/use-tenants.ts
5064
+ // src/vue/use-async-action.ts
4408
5065
  var import_vue7 = require("vue");
5066
+ function report(announce) {
5067
+ try {
5068
+ announce();
5069
+ } catch (err) {
5070
+ queueMicrotask(() => {
5071
+ throw err;
5072
+ });
5073
+ }
5074
+ }
5075
+ function useAsyncAction(fn, options = {}) {
5076
+ const pending = (0, import_vue7.ref)(false);
5077
+ const error = (0, import_vue7.ref)(null);
5078
+ const notify = options.notify ?? (0, import_vue7.inject)(SUPER_ADMIN_NOTIFY_KEY, void 0);
5079
+ const messages = useSaMessages("errors");
5080
+ const notifyOn = options.notifyOn ?? "error";
5081
+ let inFlight = 0;
5082
+ let generation = 0;
5083
+ async function run(...args) {
5084
+ const mine = ++generation;
5085
+ inFlight++;
5086
+ pending.value = true;
5087
+ error.value = null;
5088
+ try {
5089
+ const result = await fn(...args);
5090
+ await options.onSuccess?.(result);
5091
+ if (notifyOn === "both") {
5092
+ report(() => {
5093
+ const message = typeof options.successMessage === "function" ? options.successMessage() : options.successMessage;
5094
+ if (message) notify?.("positive", message);
5095
+ });
5096
+ }
5097
+ return { ok: true, value: result };
5098
+ } catch (err) {
5099
+ const adminError = toAdminError(err);
5100
+ if (mine === generation) error.value = adminError;
5101
+ if (notifyOn !== "none") {
5102
+ report(
5103
+ () => notify?.(
5104
+ "negative",
5105
+ options.errorMessage?.(adminError) ?? adminErrorMessage(adminError, messages.value)
5106
+ )
5107
+ );
5108
+ }
5109
+ return { ok: false, error: adminError };
5110
+ } finally {
5111
+ if (--inFlight === 0) pending.value = false;
5112
+ }
5113
+ }
5114
+ return {
5115
+ run,
5116
+ pending,
5117
+ error,
5118
+ reset: () => {
5119
+ error.value = null;
5120
+ }
5121
+ };
5122
+ }
5123
+
5124
+ // src/vue/use-async-data.ts
5125
+ var import_vue8 = require("vue");
5126
+ function useAsyncData(fn, options) {
5127
+ const data = (0, import_vue8.ref)(options.initial);
5128
+ const pending = (0, import_vue8.ref)(false);
5129
+ const error = (0, import_vue8.ref)(null);
5130
+ let generation = 0;
5131
+ async function reload() {
5132
+ const mine = ++generation;
5133
+ pending.value = true;
5134
+ error.value = null;
5135
+ try {
5136
+ const result = await fn();
5137
+ if (mine !== generation) return;
5138
+ data.value = result;
5139
+ } catch (err) {
5140
+ if (mine !== generation) return;
5141
+ error.value = toAdminError(err);
5142
+ data.value = options.initial;
5143
+ } finally {
5144
+ if (mine === generation) pending.value = false;
5145
+ }
5146
+ }
5147
+ if (options.watch?.length) {
5148
+ (0, import_vue8.watch)(options.watch, () => void reload());
5149
+ }
5150
+ if (options.immediate !== false) {
5151
+ void Promise.resolve().then(() => reload());
5152
+ }
5153
+ return { data, pending, error, reload };
5154
+ }
5155
+
5156
+ // src/vue/resource-registry.ts
5157
+ var import_vue9 = require("vue");
5158
+ function boundWithOverride(def, http, readContext, override) {
5159
+ const context = override?.context ? () => ({ ...readContext(), ...override.context }) : readContext;
5160
+ const platform = bindResource(def, override?.http ?? http, context);
5161
+ if (!override?.ops) return platform;
5162
+ const result = { ...platform };
5163
+ for (const [name, wrap] of Object.entries(override.ops)) {
5164
+ if (typeof wrap !== "function") continue;
5165
+ if (!Object.hasOwn(platform, name)) {
5166
+ throw new Error(
5167
+ `createResourceRegistry: resource "${def.key}" has no operation "${name}" to override. It offers: ${Object.keys(platform).join(", ")}.`
5168
+ );
5169
+ }
5170
+ const next = platform[name];
5171
+ result[name] = (...args) => wrap(next, ...args);
5172
+ }
5173
+ return result;
5174
+ }
5175
+ function createResourceRegistry(options) {
5176
+ if (!options?.http) {
5177
+ throw new Error(
5178
+ "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."
5179
+ );
5180
+ }
5181
+ const readContext = typeof options.context === "function" ? options.context : () => options.context;
5182
+ const unknown = Object.keys(options.overrides ?? {}).filter(
5183
+ (key) => !Object.hasOwn(options.resources, key)
5184
+ );
5185
+ if (unknown.length > 0) {
5186
+ throw new Error(
5187
+ `createResourceRegistry: no resource named ${unknown.map((k) => `"${k}"`).join(", ")} to override. The registry holds: ${Object.keys(options.resources).join(", ")}.`
5188
+ );
5189
+ }
5190
+ const bound = /* @__PURE__ */ new Map();
5191
+ for (const [key, def] of Object.entries(options.resources)) {
5192
+ bound.set(key, boundWithOverride(def, options.http, readContext, options.overrides?.[key]));
5193
+ }
5194
+ return {
5195
+ get(key) {
5196
+ const ops = bound.get(key);
5197
+ if (!ops) {
5198
+ throw new Error(
5199
+ `useResource("${String(key)}"): no such resource. The registry offers: ${[...bound.keys()].join(", ")}.`
5200
+ );
5201
+ }
5202
+ return ops;
5203
+ },
5204
+ keys: () => [...bound.keys()]
5205
+ };
5206
+ }
5207
+ var SUPER_ADMIN_RESOURCES_KEY = /* @__PURE__ */ Symbol.for(
5208
+ "@saasicat/ui-vue/SUPER_ADMIN_RESOURCES"
5209
+ );
5210
+ function useResource(key) {
5211
+ const registry = (0, import_vue9.inject)(SUPER_ADMIN_RESOURCES_KEY, null);
5212
+ if (!registry) {
5213
+ throw new Error(
5214
+ `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.`
5215
+ );
5216
+ }
5217
+ return registry.get(key);
5218
+ }
5219
+
5220
+ // src/vue/use-resource-list.ts
5221
+ var import_vue10 = require("vue");
5222
+ var DEFAULT_LIST_OP = "list";
5223
+ var EMPTY_PAGE = Object.freeze({
5224
+ items: Object.freeze([]),
5225
+ total: 0
5226
+ });
5227
+ function paginationCollision(key, op, claimed) {
5228
+ return new AdminError({
5229
+ 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.`
5230
+ });
5231
+ }
5232
+ function paginationKeysIn(filter) {
5233
+ return LIST_PAGINATION_PARAMS.filter((param) => isSentInQuery(filter[param]));
5234
+ }
5235
+ function readListOp(key, ops, name) {
5236
+ const op = Object.hasOwn(ops, name) ? ops[name] : void 0;
5237
+ if (typeof op !== "function") {
5238
+ throw new Error(
5239
+ `useResourceList("${key}", { op: "${name}" }): that resource has no such operation. It offers: ${Object.keys(ops).join(", ")}.`
5240
+ );
5241
+ }
5242
+ return op;
5243
+ }
5244
+ function useResourceList(key, opts) {
5245
+ const ops = useResource(key);
5246
+ const opName = opts?.op ?? DEFAULT_LIST_OP;
5247
+ const load = readListOp(key, ops, opName);
5248
+ if (opts?.filter) {
5249
+ const claimed = paginationKeysIn(opts.filter.value);
5250
+ if (claimed.length > 0) throw paginationCollision(key, opName, claimed);
5251
+ }
5252
+ const page = (0, import_vue10.ref)(LIST_FIRST_PAGE);
5253
+ const pageSize = (0, import_vue10.ref)(clampListPageSize(opts?.pageSize ?? LIST_PAGE_SIZE_DEFAULT));
5254
+ function query() {
5255
+ const filter = opts?.filter?.value;
5256
+ if (filter) {
5257
+ const claimed = paginationKeysIn(filter);
5258
+ if (claimed.length > 0) throw paginationCollision(key, opName, claimed);
5259
+ }
5260
+ return { page: page.value, pageSize: pageSize.value, filter };
5261
+ }
5262
+ const state = useAsyncData(() => load(query()), {
5263
+ initial: EMPTY_PAGE,
5264
+ immediate: opts?.immediate
5265
+ });
5266
+ (0, import_vue10.watch)(state.data, (answer) => {
5267
+ if (typeof answer.page === "number") page.value = answer.page;
5268
+ if (typeof answer.pageSize === "number") pageSize.value = answer.pageSize;
5269
+ });
5270
+ async function goToPage(next) {
5271
+ page.value = clampListPage(next);
5272
+ await state.reload();
5273
+ }
5274
+ async function setPageSize(size) {
5275
+ pageSize.value = clampListPageSize(size);
5276
+ page.value = LIST_FIRST_PAGE;
5277
+ await state.reload();
5278
+ }
5279
+ if (opts?.filter) {
5280
+ (0, import_vue10.watch)(
5281
+ opts.filter,
5282
+ () => {
5283
+ page.value = LIST_FIRST_PAGE;
5284
+ void state.reload();
5285
+ },
5286
+ { deep: true }
5287
+ );
5288
+ }
5289
+ return {
5290
+ items: (0, import_vue10.computed)(() => state.data.value.items),
5291
+ total: (0, import_vue10.computed)(() => state.data.value.total ?? state.data.value.items.length),
5292
+ page,
5293
+ pageSize,
5294
+ pending: state.pending,
5295
+ error: state.error,
5296
+ reload: state.reload,
5297
+ goToPage,
5298
+ setPageSize
5299
+ };
5300
+ }
5301
+
5302
+ // src/vue/use-tenants.ts
5303
+ var import_vue11 = require("vue");
4409
5304
  function useTenants(options) {
4410
5305
  if (!options?.endpoint) {
4411
5306
  throw new Error(
4412
5307
  '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
5308
  );
4414
5309
  }
4415
- const filter = options.filter ?? (0, import_vue7.ref)({});
5310
+ const filter = options.filter ?? (0, import_vue11.ref)({});
4416
5311
  const list = useApiList({
4417
5312
  endpoint: options.endpoint,
4418
5313
  filter,
@@ -4424,14 +5319,14 @@ function useTenants(options) {
4424
5319
  }
4425
5320
 
4426
5321
  // src/vue/use-audit-entries.ts
4427
- var import_vue8 = require("vue");
5322
+ var import_vue12 = require("vue");
4428
5323
  function useAuditEntries(options) {
4429
5324
  if (!options?.endpoint) {
4430
5325
  throw new Error(
4431
5326
  '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
5327
  );
4433
5328
  }
4434
- const filter = options.filter ?? (0, import_vue8.ref)({});
5329
+ const filter = options.filter ?? (0, import_vue12.ref)({});
4435
5330
  const list = useApiList({
4436
5331
  endpoint: options.endpoint,
4437
5332
  filter,
@@ -4443,7 +5338,7 @@ function useAuditEntries(options) {
4443
5338
  }
4444
5339
 
4445
5340
  // src/vue/use-entitlement.ts
4446
- var import_vue9 = require("vue");
5341
+ var import_vue13 = require("vue");
4447
5342
  function useEntitlement(options) {
4448
5343
  if (!options?.endpoint) {
4449
5344
  throw new Error(
@@ -4452,9 +5347,9 @@ function useEntitlement(options) {
4452
5347
  }
4453
5348
  const endpoint = options.endpoint;
4454
5349
  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);
5350
+ const entitlement = (0, import_vue13.ref)(null);
5351
+ const loading = (0, import_vue13.ref)(false);
5352
+ const error = (0, import_vue13.ref)(null);
4458
5353
  async function load() {
4459
5354
  loading.value = true;
4460
5355
  error.value = null;
@@ -4484,15 +5379,15 @@ function useEntitlement(options) {
4484
5379
  }
4485
5380
 
4486
5381
  // src/vue/use-tenant-manifest.ts
4487
- var import_vue10 = require("vue");
5382
+ var import_vue14 = require("vue");
4488
5383
  function useTenantManifest(options) {
4489
5384
  if (!options?.endpoint) {
4490
5385
  throw new Error('useTenantManifest: `endpoint` is required (e.g. "/api/tenant/manifest").');
4491
5386
  }
4492
5387
  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);
5388
+ const manifest = (0, import_vue14.ref)(null);
5389
+ const loading = (0, import_vue14.ref)(false);
5390
+ const error = (0, import_vue14.ref)(null);
4496
5391
  async function load() {
4497
5392
  loading.value = true;
4498
5393
  error.value = null;
@@ -4526,7 +5421,7 @@ function useTenantManifest(options) {
4526
5421
  }
4527
5422
 
4528
5423
  // src/vue/entitlement-provider.ts
4529
- var import_vue11 = require("vue");
5424
+ var import_vue15 = require("vue");
4530
5425
  var ENTITLEMENT_INJECTION_KEY = /* @__PURE__ */ Symbol.for(
4531
5426
  "@saasicat/ui-vue/ENTITLEMENT"
4532
5427
  );
@@ -4534,7 +5429,7 @@ function provideEntitlement(app, entitlement) {
4534
5429
  app.provide(ENTITLEMENT_INJECTION_KEY, entitlement);
4535
5430
  }
4536
5431
  function useInjectedEntitlement() {
4537
- return (0, import_vue11.inject)(ENTITLEMENT_INJECTION_KEY, null);
5432
+ return (0, import_vue15.inject)(ENTITLEMENT_INJECTION_KEY, null);
4538
5433
  }
4539
5434
 
4540
5435
  // src/vue/feature-router-guard.ts
@@ -4563,15 +5458,15 @@ function buildFeatureRouterGuard(options) {
4563
5458
  }
4564
5459
 
4565
5460
  // src/vue/use-tenant-billing-catalog.ts
4566
- var import_vue12 = require("vue");
5461
+ var import_vue16 = require("vue");
4567
5462
  function useTenantBillingCatalog(options = {}) {
4568
5463
  const apiPrefix = trimTrailingSlashes(options.apiPrefix ?? "/billing");
4569
5464
  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);
5465
+ const plans = (0, import_vue16.ref)(null);
5466
+ const featureRegistry = (0, import_vue16.ref)(null);
5467
+ const bundles = (0, import_vue16.ref)(null);
5468
+ const loading = (0, import_vue16.ref)(false);
5469
+ const error = (0, import_vue16.ref)(null);
4575
5470
  async function fetchJson(path) {
4576
5471
  const res = await http(`${apiPrefix}${path}`, { method: "GET" });
4577
5472
  if (res.status !== 200) {
@@ -4621,14 +5516,14 @@ function useTenantBillingCatalog(options = {}) {
4621
5516
  }
4622
5517
 
4623
5518
  // src/vue/use-tenant-billing.ts
4624
- var import_vue13 = require("vue");
5519
+ var import_vue17 = require("vue");
4625
5520
  function useTenantBilling(options = {}) {
4626
5521
  const apiPrefix = trimTrailingSlashes(options.apiPrefix ?? "/billing");
4627
5522
  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)([]);
5523
+ const usage = (0, import_vue17.ref)(null);
5524
+ const loading = (0, import_vue17.ref)(false);
5525
+ const error = (0, import_vue17.ref)(null);
5526
+ const subscriptionBundles = (0, import_vue17.ref)([]);
4632
5527
  function authHeaders() {
4633
5528
  const headers = {};
4634
5529
  const token = options.getAuthToken?.();
@@ -4762,7 +5657,7 @@ function useTenantBilling(options = {}) {
4762
5657
  }
4763
5658
 
4764
5659
  // src/vue/use-subscription-draft.ts
4765
- var import_vue14 = require("vue");
5660
+ var import_vue18 = require("vue");
4766
5661
  var import_types13 = require("@saasicat/types");
4767
5662
  var DEFAULT_YEARLY_FACTOR = 10;
4768
5663
  function unwrap(source) {
@@ -4779,27 +5674,27 @@ function priceForCycle(monthlyNet, yearlyNet, cycle, yearlyFactor) {
4779
5674
  }
4780
5675
  function useSubscriptionDraft(options) {
4781
5676
  const yearlyFactor = options.yearlyFactor ?? DEFAULT_YEARLY_FACTOR;
4782
- const plansRef = (0, import_vue14.computed)(() => unwrap(options.plans) ?? []);
4783
- const subscriptionBundlesRef = (0, import_vue14.computed)(
5677
+ const plansRef = (0, import_vue18.computed)(() => unwrap(options.plans) ?? []);
5678
+ const subscriptionBundlesRef = (0, import_vue18.computed)(
4784
5679
  () => unwrap(options.subscriptionBundles ?? null) ?? []
4785
5680
  );
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)(
5681
+ const plan = (0, import_vue18.ref)(options.initialPlan ?? null);
5682
+ const cycle = (0, import_vue18.ref)(options.initialCycle ?? "MONTHLY");
5683
+ const selectedBundleVersionIds = (0, import_vue18.ref)(
4789
5684
  new Set(options.initialBundleVersionIds ?? [])
4790
5685
  );
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)(() => {
5686
+ const promoCode = (0, import_vue18.ref)("");
5687
+ const promoState = (0, import_vue18.ref)({ status: "idle", preview: null, message: "" });
5688
+ const selectedPlan = (0, import_vue18.computed)(() => {
4794
5689
  if (!plan.value) return null;
4795
5690
  return plansRef.value.find((p) => p.id === plan.value) ?? null;
4796
5691
  });
4797
- const selectedBundles = (0, import_vue14.computed)(
5692
+ const selectedBundles = (0, import_vue18.computed)(
4798
5693
  () => subscriptionBundlesRef.value.filter(
4799
5694
  (b) => selectedBundleVersionIds.value.has(b.bundleVersionId)
4800
5695
  )
4801
5696
  );
4802
- const chargeableBundles = (0, import_vue14.computed)(() => {
5697
+ const chargeableBundles = (0, import_vue18.computed)(() => {
4803
5698
  const planFeatures = selectedPlan.value?.features ?? [];
4804
5699
  const selected = selectedBundles.value;
4805
5700
  const keptIds = new Set(
@@ -4807,7 +5702,7 @@ function useSubscriptionDraft(options) {
4807
5702
  );
4808
5703
  return selected.filter((b) => keptIds.has(b.bundleVersionId));
4809
5704
  });
4810
- const activeFeatures = (0, import_vue14.computed)(() => {
5705
+ const activeFeatures = (0, import_vue18.computed)(() => {
4811
5706
  const result = /* @__PURE__ */ new Set();
4812
5707
  const planObj = selectedPlan.value;
4813
5708
  if (planObj) {
@@ -4818,7 +5713,7 @@ function useSubscriptionDraft(options) {
4818
5713
  }
4819
5714
  return result;
4820
5715
  });
4821
- const pricing = (0, import_vue14.computed)(() => {
5716
+ const pricing = (0, import_vue18.computed)(() => {
4822
5717
  const cyc = cycle.value;
4823
5718
  const planObj = selectedPlan.value;
4824
5719
  const planNet = planObj ? priceForCycle(planObj.monthlyNet ?? 0, planObj.yearlyNet, cyc, yearlyFactor) : 0;
@@ -4878,7 +5773,7 @@ function useSubscriptionDraft(options) {
4878
5773
  }
4879
5774
  };
4880
5775
  });
4881
- const isDirty = (0, import_vue14.computed)(() => {
5776
+ const isDirty = (0, import_vue18.computed)(() => {
4882
5777
  return selectedBundleVersionIds.value.size > 0 || promoState.value.status === "valid";
4883
5778
  });
4884
5779
  function setPlan(planId) {
@@ -4954,7 +5849,7 @@ function useSubscriptionDraft(options) {
4954
5849
  }
4955
5850
 
4956
5851
  // src/vue/use-bulk-publish.ts
4957
- var import_vue15 = require("vue");
5852
+ var import_vue19 = require("vue");
4958
5853
  function useBulkPublish(options) {
4959
5854
  const msg = useSaMessages("planVersions");
4960
5855
  if (!options?.endpoints) {
@@ -4964,15 +5859,15 @@ function useBulkPublish(options) {
4964
5859
  }
4965
5860
  const http = options.http ?? defaultHttpClient();
4966
5861
  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)(() => {
5862
+ const items = (0, import_vue19.ref)([]);
5863
+ const successCount = (0, import_vue19.computed)(() => items.value.filter((i) => i.status === "published").length);
5864
+ const failureCount = (0, import_vue19.computed)(() => items.value.filter((i) => i.status === "failed").length);
5865
+ const progress = (0, import_vue19.computed)(() => {
4971
5866
  if (items.value.length === 0) return 0;
4972
5867
  const done2 = successCount.value + failureCount.value;
4973
5868
  return done2 / items.value.length;
4974
5869
  });
4975
- const done = (0, import_vue15.computed)(
5870
+ const done = (0, import_vue19.computed)(
4976
5871
  () => items.value.length > 0 && items.value.every((i) => i.status === "published" || i.status === "failed")
4977
5872
  );
4978
5873
  function setItems(next) {
@@ -5030,7 +5925,7 @@ function useBulkPublish(options) {
5030
5925
  }
5031
5926
 
5032
5927
  // src/vue/use-plan-editor.ts
5033
- var import_vue16 = require("vue");
5928
+ var import_vue20 = require("vue");
5034
5929
  var PlannedOnlyFeatureError = class extends Error {
5035
5930
  constructor(violations) {
5036
5931
  super(
@@ -5050,10 +5945,10 @@ function tierWeight(tier) {
5050
5945
  function usePlanEditor(manifest, options = {}) {
5051
5946
  const baseSet = new Set(options.baseFeatures ?? []);
5052
5947
  const nonRegressive = options.nonRegressive ?? true;
5053
- const selectedFeatures = (0, import_vue16.ref)(new Set(options.initialFeatures ?? []));
5948
+ const selectedFeatures = (0, import_vue20.ref)(new Set(options.initialFeatures ?? []));
5054
5949
  const catalogFeatures = manifest.planCatalogSnapshot.features ?? [];
5055
5950
  const plannedOnlySet = new Set(catalogFeatures.filter((f) => f.plannedOnly).map((f) => f.key));
5056
- const availableFeatures = (0, import_vue16.computed)(() => {
5951
+ const availableFeatures = (0, import_vue20.computed)(() => {
5057
5952
  return catalogFeatures.map((def) => {
5058
5953
  const isSelected = selectedFeatures.value.has(def.key);
5059
5954
  const isPlannedOnly = !!def.plannedOnly;
@@ -5062,7 +5957,7 @@ function usePlanEditor(manifest, options = {}) {
5062
5957
  return { def, isSelected, isPlannedOnly, isInherited, canToggle };
5063
5958
  });
5064
5959
  });
5065
- const featuresByTier = (0, import_vue16.computed)(() => {
5960
+ const featuresByTier = (0, import_vue20.computed)(() => {
5066
5961
  const groups = /* @__PURE__ */ new Map();
5067
5962
  for (const row of availableFeatures.value) {
5068
5963
  const tier = row.def.tier ?? "OTHER";
@@ -5107,12 +6002,12 @@ function usePlanEditor(manifest, options = {}) {
5107
6002
  }
5108
6003
 
5109
6004
  // src/vue/use-public-boot.ts
5110
- var import_vue17 = require("vue");
6005
+ var import_vue21 = require("vue");
5111
6006
  function usePublicBoot(options) {
5112
6007
  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);
6008
+ const boot = (0, import_vue21.ref)(null);
6009
+ const loading = (0, import_vue21.ref)(false);
6010
+ const error = (0, import_vue21.ref)(null);
5116
6011
  async function load() {
5117
6012
  loading.value = true;
5118
6013
  error.value = null;
@@ -5129,27 +6024,27 @@ function usePublicBoot(options) {
5129
6024
  }
5130
6025
 
5131
6026
  // src/vue/use-discovery.ts
5132
- var import_vue18 = require("vue");
6027
+ var import_vue22 = require("vue");
5133
6028
  var DiscoveryLoadError = class extends Error {
5134
6029
  constructor(status, message) {
5135
6030
  super(message);
5136
6031
  this.status = status;
5137
6032
  this.name = "DiscoveryLoadError";
6033
+ markPlatformError(this);
5138
6034
  }
5139
6035
  status;
5140
6036
  };
5141
6037
  function useDiscovery(options) {
5142
- const msg = useSaMessages("discovery");
5143
6038
  if (!options?.endpoint) {
5144
6039
  throw new Error(
5145
6040
  '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
6041
  );
5147
6042
  }
5148
6043
  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);
6044
+ const snapshot = (0, import_vue22.ref)(null);
6045
+ const etag = (0, import_vue22.ref)(null);
6046
+ const loading = (0, import_vue22.ref)(false);
6047
+ const error = (0, import_vue22.ref)(null);
5153
6048
  async function fetchSnapshot(includeEtag) {
5154
6049
  loading.value = true;
5155
6050
  error.value = null;
@@ -5161,13 +6056,19 @@ function useDiscovery(options) {
5161
6056
  headers["If-None-Match"] = etag.value;
5162
6057
  }
5163
6058
  const res = await http(options.endpoint, { method: "GET", headers });
6059
+ requireServerAnswer(
6060
+ res.status,
6061
+ "GET",
6062
+ options.endpoint,
6063
+ (diagnostic) => new DiscoveryLoadError(res.status, diagnostic)
6064
+ );
5164
6065
  if (res.status === 304) {
5165
6066
  return;
5166
6067
  }
5167
6068
  if (res.status !== 200) {
5168
6069
  throw new DiscoveryLoadError(
5169
6070
  res.status,
5170
- formatMessage(msg.value.errorDiscoveryHttp, { status: res.status })
6071
+ `Discovery endpoint responded with HTTP ${res.status}`
5171
6072
  );
5172
6073
  }
5173
6074
  const body = await res.json();
@@ -5193,11 +6094,18 @@ function useDiscovery(options) {
5193
6094
  const headers = { "content-type": "application/json" };
5194
6095
  const token = options.getAuthToken?.();
5195
6096
  if (token) headers.Authorization = `Bearer ${token}`;
5196
- const res = await http(`${options.endpoint}/rescan`, { method: "POST", headers });
6097
+ const rescanUrl = `${options.endpoint}/rescan`;
6098
+ const res = await http(rescanUrl, { method: "POST", headers });
6099
+ requireServerAnswer(
6100
+ res.status,
6101
+ "POST",
6102
+ rescanUrl,
6103
+ (diagnostic) => new DiscoveryLoadError(res.status, diagnostic)
6104
+ );
5197
6105
  if (res.status !== 200 && res.status !== 201) {
5198
6106
  throw new DiscoveryLoadError(
5199
6107
  res.status,
5200
- formatMessage(msg.value.errorRescanHttp, { status: res.status })
6108
+ `Discovery rescan responded with HTTP ${res.status}`
5201
6109
  );
5202
6110
  }
5203
6111
  const body = await res.json();
@@ -5216,13 +6124,14 @@ function useDiscovery(options) {
5216
6124
  }
5217
6125
 
5218
6126
  // src/vue/use-catalog-entries.ts
5219
- var import_vue19 = require("vue");
6127
+ var import_vue23 = require("vue");
5220
6128
  var CatalogEntriesApiError = class extends Error {
5221
6129
  constructor(status, body, message) {
5222
6130
  super(message);
5223
6131
  this.status = status;
5224
6132
  this.body = body;
5225
6133
  this.name = "CatalogEntriesApiError";
6134
+ markPlatformError(this);
5226
6135
  }
5227
6136
  status;
5228
6137
  body;
@@ -5234,13 +6143,12 @@ function useCatalogEntries(options) {
5234
6143
  if (!options?.projectKey) {
5235
6144
  throw new Error("useCatalogEntries: `projectKey` is required.");
5236
6145
  }
5237
- const msg = useSaMessages("discovery");
5238
6146
  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);
6147
+ const capabilities = (0, import_vue23.ref)([]);
6148
+ const features = (0, import_vue23.ref)([]);
6149
+ const quotas = (0, import_vue23.ref)([]);
6150
+ const loading = (0, import_vue23.ref)(false);
6151
+ const error = (0, import_vue23.ref)(null);
5244
6152
  const base = `${options.adminEndpoint}/catalog`;
5245
6153
  const pk = encodeURIComponent(options.projectKey);
5246
6154
  function authHeaders() {
@@ -5248,18 +6156,25 @@ function useCatalogEntries(options) {
5248
6156
  return token ? { Authorization: `Bearer ${token}` } : {};
5249
6157
  }
5250
6158
  async function fetchJson(url, init) {
6159
+ const method = init?.method ?? "GET";
5251
6160
  const res = await http(url, {
5252
- method: init?.method ?? "GET",
6161
+ method,
5253
6162
  headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
5254
6163
  body: init?.body
5255
6164
  });
6165
+ requireServerAnswer(
6166
+ res.status,
6167
+ method,
6168
+ url,
6169
+ (diagnostic) => new CatalogEntriesApiError(res.status, null, diagnostic)
6170
+ );
5256
6171
  if (res.status === 204) return null;
5257
6172
  const body = await res.json().catch(() => null);
5258
6173
  if (res.status >= 400) {
5259
6174
  throw new CatalogEntriesApiError(
5260
6175
  res.status,
5261
6176
  body,
5262
- formatMessage(msg.value.errorCatalogHttp, { status: res.status })
6177
+ `Catalog entries API responded with HTTP ${res.status}`
5263
6178
  );
5264
6179
  }
5265
6180
  return body;
@@ -5287,7 +6202,8 @@ function useCatalogEntries(options) {
5287
6202
  `${base}/features/${encodeURIComponent(featureKey)}/review?projectKey=${pk}`,
5288
6203
  { method: "PATCH", body: JSON.stringify(data) }
5289
6204
  );
5290
- if (!updated) throw new CatalogEntriesApiError(0, null, "Review returned no body");
6205
+ if (!updated)
6206
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "Review returned no body"));
5291
6207
  features.value = features.value.map((f) => f.featureKey === featureKey ? updated : f);
5292
6208
  return updated;
5293
6209
  }
@@ -5296,7 +6212,8 @@ function useCatalogEntries(options) {
5296
6212
  `${base}/quotas/${encodeURIComponent(quotaKey)}/review?projectKey=${pk}`,
5297
6213
  { method: "PATCH", body: JSON.stringify(data) }
5298
6214
  );
5299
- if (!updated) throw new CatalogEntriesApiError(0, null, "Review returned no body");
6215
+ if (!updated)
6216
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "Review returned no body"));
5300
6217
  quotas.value = quotas.value.map((q) => q.quotaKey === quotaKey ? updated : q);
5301
6218
  return updated;
5302
6219
  }
@@ -5305,7 +6222,8 @@ function useCatalogEntries(options) {
5305
6222
  `${base}/features/${encodeURIComponent(featureKey)}/i18n?projectKey=${pk}`,
5306
6223
  { method: "PATCH", body: JSON.stringify({ i18n }) }
5307
6224
  );
5308
- if (!updated) throw new CatalogEntriesApiError(0, null, "i18n returned no body");
6225
+ if (!updated)
6226
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "i18n returned no body"));
5309
6227
  features.value = features.value.map((f) => f.featureKey === featureKey ? updated : f);
5310
6228
  return updated;
5311
6229
  }
@@ -5314,7 +6232,8 @@ function useCatalogEntries(options) {
5314
6232
  `${base}/quotas/${encodeURIComponent(quotaKey)}/i18n?projectKey=${pk}`,
5315
6233
  { method: "PATCH", body: JSON.stringify({ i18n }) }
5316
6234
  );
5317
- if (!updated) throw new CatalogEntriesApiError(0, null, "i18n returned no body");
6235
+ if (!updated)
6236
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "i18n returned no body"));
5318
6237
  quotas.value = quotas.value.map((q) => q.quotaKey === quotaKey ? updated : q);
5319
6238
  return updated;
5320
6239
  }
@@ -5323,7 +6242,8 @@ function useCatalogEntries(options) {
5323
6242
  `${base}/features/${encodeURIComponent(featureKey)}?projectKey=${pk}`,
5324
6243
  { method: "PATCH", body: JSON.stringify(data) }
5325
6244
  );
5326
- if (!updated) throw new CatalogEntriesApiError(0, null, "Base returned no body");
6245
+ if (!updated)
6246
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "Base returned no body"));
5327
6247
  features.value = features.value.map((f) => f.featureKey === featureKey ? updated : f);
5328
6248
  return updated;
5329
6249
  }
@@ -5332,7 +6252,8 @@ function useCatalogEntries(options) {
5332
6252
  `${base}/quotas/${encodeURIComponent(quotaKey)}?projectKey=${pk}`,
5333
6253
  { method: "PATCH", body: JSON.stringify(data) }
5334
6254
  );
5335
- if (!updated) throw new CatalogEntriesApiError(0, null, "Base returned no body");
6255
+ if (!updated)
6256
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "Base returned no body"));
5336
6257
  quotas.value = quotas.value.map((q) => q.quotaKey === quotaKey ? updated : q);
5337
6258
  return updated;
5338
6259
  }
@@ -5341,7 +6262,8 @@ function useCatalogEntries(options) {
5341
6262
  method: "POST",
5342
6263
  body: JSON.stringify({ snapshot })
5343
6264
  });
5344
- if (!result) throw new CatalogEntriesApiError(0, null, "Sync returned no body");
6265
+ if (!result)
6266
+ throw markEmptyResponse(new CatalogEntriesApiError(0, null, "Sync returned no body"));
5345
6267
  await load();
5346
6268
  return result;
5347
6269
  }
@@ -5366,13 +6288,14 @@ function useCatalogEntries(options) {
5366
6288
  }
5367
6289
 
5368
6290
  // src/vue/use-bundles.ts
5369
- var import_vue20 = require("vue");
6291
+ var import_vue24 = require("vue");
5370
6292
  var BundlesApiError = class extends Error {
5371
6293
  constructor(status, body, message) {
5372
6294
  super(message);
5373
6295
  this.status = status;
5374
6296
  this.body = body;
5375
6297
  this.name = "BundlesApiError";
6298
+ markPlatformError(this);
5376
6299
  }
5377
6300
  status;
5378
6301
  body;
@@ -5387,20 +6310,27 @@ function useBundles(options) {
5387
6310
  throw new Error("useBundles: `projectKey` is required.");
5388
6311
  }
5389
6312
  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);
6313
+ const bundles = (0, import_vue24.ref)([]);
6314
+ const loading = (0, import_vue24.ref)(false);
6315
+ const error = (0, import_vue24.ref)(null);
5393
6316
  const baseUrl = `${options.adminEndpoint}/catalog/bundles`;
5394
6317
  function authHeaders() {
5395
6318
  const token = options.getAuthToken?.();
5396
6319
  return token ? { Authorization: `Bearer ${token}` } : {};
5397
6320
  }
5398
6321
  async function fetchJson(url, init) {
6322
+ const method = init?.method ?? "GET";
5399
6323
  const res = await http(url, {
5400
- method: init?.method ?? "GET",
6324
+ method,
5401
6325
  headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
5402
6326
  body: init?.body
5403
6327
  });
6328
+ requireServerAnswer(
6329
+ res.status,
6330
+ method,
6331
+ url,
6332
+ (diagnostic) => new BundlesApiError(res.status, null, diagnostic)
6333
+ );
5404
6334
  if (res.status === 204) return null;
5405
6335
  const body = await res.json().catch(() => null);
5406
6336
  if (res.status >= 400) {
@@ -5431,7 +6361,8 @@ function useBundles(options) {
5431
6361
  method: "POST",
5432
6362
  body: JSON.stringify(data)
5433
6363
  });
5434
- if (!created) throw new BundlesApiError(0, null, "Create returned no body");
6364
+ if (!created)
6365
+ throw markEmptyResponse(new BundlesApiError(0, null, "Create returned no body"));
5435
6366
  bundles.value = [...bundles.value, created];
5436
6367
  return created;
5437
6368
  }
@@ -5440,7 +6371,8 @@ function useBundles(options) {
5440
6371
  method: "PATCH",
5441
6372
  body: JSON.stringify(data)
5442
6373
  });
5443
- if (!updated) throw new BundlesApiError(0, null, "Update returned no body");
6374
+ if (!updated)
6375
+ throw markEmptyResponse(new BundlesApiError(0, null, "Update returned no body"));
5444
6376
  bundles.value = bundles.value.map((b) => b.id === bundleId ? updated : b);
5445
6377
  return updated;
5446
6378
  }
@@ -5461,9 +6393,9 @@ function useBundleVersions(options) {
5461
6393
  throw new Error("useBundleVersions: `bundleId` is required.");
5462
6394
  }
5463
6395
  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);
6396
+ const versions = (0, import_vue24.ref)([]);
6397
+ const loading = (0, import_vue24.ref)(false);
6398
+ const error = (0, import_vue24.ref)(null);
5467
6399
  const bundleVersionsUrl = `${options.adminEndpoint}/catalog/bundles/${options.bundleId}/versions`;
5468
6400
  const versionUrlBase = `${options.adminEndpoint}/catalog/bundle-versions`;
5469
6401
  function authHeaders() {
@@ -5471,11 +6403,18 @@ function useBundleVersions(options) {
5471
6403
  return token ? { Authorization: `Bearer ${token}` } : {};
5472
6404
  }
5473
6405
  async function fetchJson(url, init) {
6406
+ const method = init?.method ?? "GET";
5474
6407
  const res = await http(url, {
5475
- method: init?.method ?? "GET",
6408
+ method,
5476
6409
  headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
5477
6410
  body: init?.body
5478
6411
  });
6412
+ requireServerAnswer(
6413
+ res.status,
6414
+ method,
6415
+ url,
6416
+ (diagnostic) => new BundlesApiError(res.status, null, diagnostic)
6417
+ );
5479
6418
  if (res.status === 204) return null;
5480
6419
  const body = await res.json().catch(() => null);
5481
6420
  if (res.status >= 400) {
@@ -5504,7 +6443,8 @@ function useBundleVersions(options) {
5504
6443
  method: "POST",
5505
6444
  body: JSON.stringify(data)
5506
6445
  });
5507
- if (!result) throw new BundlesApiError(0, null, "CreateDraft returned no body");
6446
+ if (!result)
6447
+ throw markEmptyResponse(new BundlesApiError(0, null, "CreateDraft returned no body"));
5508
6448
  versions.value = [...versions.value, result.bundleVersion];
5509
6449
  return result;
5510
6450
  }
@@ -5513,7 +6453,8 @@ function useBundleVersions(options) {
5513
6453
  `${versionUrlBase}/${versionId}`,
5514
6454
  { method: "PATCH", body: JSON.stringify(data) }
5515
6455
  );
5516
- if (!result) throw new BundlesApiError(0, null, "UpdateDraft returned no body");
6456
+ if (!result)
6457
+ throw markEmptyResponse(new BundlesApiError(0, null, "UpdateDraft returned no body"));
5517
6458
  versions.value = versions.value.map((v) => v.id === versionId ? result.bundleVersion : v);
5518
6459
  return result;
5519
6460
  }
@@ -5522,7 +6463,8 @@ function useBundleVersions(options) {
5522
6463
  `${versionUrlBase}/${versionId}/publish`,
5523
6464
  { method: "POST", body: JSON.stringify(opts) }
5524
6465
  );
5525
- if (!result) throw new BundlesApiError(0, null, "Publish returned no body");
6466
+ if (!result)
6467
+ throw markEmptyResponse(new BundlesApiError(0, null, "Publish returned no body"));
5526
6468
  await load();
5527
6469
  return result;
5528
6470
  }
@@ -5537,7 +6479,7 @@ function useBundleVersions(options) {
5537
6479
  }
5538
6480
 
5539
6481
  // src/vue/use-bundle-versions-map.ts
5540
- var import_vue21 = require("vue");
6482
+ var import_vue25 = require("vue");
5541
6483
  function useBundleVersionsMap(options) {
5542
6484
  if (!options?.adminEndpoint) {
5543
6485
  throw new Error("useBundleVersionsMap: `adminEndpoint` is required.");
@@ -5546,9 +6488,9 @@ function useBundleVersionsMap(options) {
5546
6488
  throw new Error("useBundleVersionsMap: `bundles` is required.");
5547
6489
  }
5548
6490
  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);
6491
+ const versionsByBundle = (0, import_vue25.ref)({});
6492
+ const loading = (0, import_vue25.ref)(false);
6493
+ const error = (0, import_vue25.ref)(null);
5552
6494
  function authHeaders() {
5553
6495
  const token = options.getAuthToken?.();
5554
6496
  return token ? { Authorization: `Bearer ${token}` } : {};
@@ -5598,7 +6540,7 @@ function useBundleVersionsMap(options) {
5598
6540
  console.warn(`useBundleVersionsMap: refreshOne('${bundleId}') failed`, err);
5599
6541
  }
5600
6542
  }
5601
- (0, import_vue21.watch)(
6543
+ (0, import_vue25.watch)(
5602
6544
  () => options.bundles.value.map((b) => b.id).join(","),
5603
6545
  () => {
5604
6546
  if (options.bundles.value.length > 0) void refresh();
@@ -5610,13 +6552,14 @@ function useBundleVersionsMap(options) {
5610
6552
  }
5611
6553
 
5612
6554
  // src/vue/use-tenant-subscription-bundles.ts
5613
- var import_vue22 = require("vue");
6555
+ var import_vue26 = require("vue");
5614
6556
  var TenantSubscriptionBundlesApiError = class extends Error {
5615
6557
  constructor(status, body, message) {
5616
6558
  super(message);
5617
6559
  this.status = status;
5618
6560
  this.body = body;
5619
6561
  this.name = "TenantSubscriptionBundlesApiError";
6562
+ markPlatformError(this);
5620
6563
  }
5621
6564
  status;
5622
6565
  body;
@@ -5629,19 +6572,26 @@ function useTenantSubscriptionBundles(options) {
5629
6572
  }
5630
6573
  const http = options.http ?? defaultHttpClient();
5631
6574
  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);
6575
+ const bundles = (0, import_vue26.ref)([]);
6576
+ const loading = (0, import_vue26.ref)(false);
6577
+ const error = (0, import_vue26.ref)(null);
5635
6578
  function authHeaders() {
5636
6579
  const token = options.getAuthToken?.();
5637
6580
  return token ? { Authorization: `Bearer ${token}` } : {};
5638
6581
  }
5639
6582
  async function fetchJson(url, init) {
6583
+ const method = init?.method ?? "GET";
5640
6584
  const res = await http(url, {
5641
- method: init?.method ?? "GET",
6585
+ method,
5642
6586
  headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
5643
6587
  body: init?.body
5644
6588
  });
6589
+ requireServerAnswer(
6590
+ res.status,
6591
+ method,
6592
+ url,
6593
+ (diagnostic) => new TenantSubscriptionBundlesApiError(res.status, null, diagnostic)
6594
+ );
5645
6595
  if (res.status === 204) return null;
5646
6596
  const body = await res.json().catch(() => null);
5647
6597
  if (res.status >= 400) {
@@ -5671,7 +6621,9 @@ function useTenantSubscriptionBundles(options) {
5671
6621
  body: JSON.stringify(data)
5672
6622
  });
5673
6623
  if (!result) {
5674
- throw new TenantSubscriptionBundlesApiError(0, null, "add returned no body");
6624
+ throw markEmptyResponse(
6625
+ new TenantSubscriptionBundlesApiError(0, null, "add returned no body")
6626
+ );
5675
6627
  }
5676
6628
  const hydrated = rehydrateDates(result);
5677
6629
  bundles.value = [hydrated, ...bundles.value];
@@ -5683,7 +6635,9 @@ function useTenantSubscriptionBundles(options) {
5683
6635
  { method: "DELETE", body: JSON.stringify(opts) }
5684
6636
  );
5685
6637
  if (!result) {
5686
- throw new TenantSubscriptionBundlesApiError(0, null, "cancel returned no body");
6638
+ throw markEmptyResponse(
6639
+ new TenantSubscriptionBundlesApiError(0, null, "cancel returned no body")
6640
+ );
5687
6641
  }
5688
6642
  const hydrated = rehydrateDates(result);
5689
6643
  bundles.value = bundles.value.map((b) => b.id === subscriptionBundleId ? hydrated : b);
@@ -5705,13 +6659,14 @@ function rehydrateDates(raw) {
5705
6659
  }
5706
6660
 
5707
6661
  // src/vue/use-marketing-projections.ts
5708
- var import_vue23 = require("vue");
6662
+ var import_vue27 = require("vue");
5709
6663
  var MarketingProjectionsApiError = class extends Error {
5710
6664
  constructor(status, body, message) {
5711
6665
  super(message);
5712
6666
  this.status = status;
5713
6667
  this.body = body;
5714
6668
  this.name = "MarketingProjectionsApiError";
6669
+ markPlatformError(this);
5715
6670
  }
5716
6671
  status;
5717
6672
  body;
@@ -5723,12 +6678,11 @@ function useMarketingProjections(options) {
5723
6678
  if (!options?.filter?.projectKey) {
5724
6679
  throw new Error("useMarketingProjections: `filter.projectKey` is required.");
5725
6680
  }
5726
- const msg = useSaMessages("marketing");
5727
6681
  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);
6682
+ const projections = (0, import_vue27.ref)([]);
6683
+ const filter = (0, import_vue27.ref)({ ...options.filter });
6684
+ const loading = (0, import_vue27.ref)(false);
6685
+ const error = (0, import_vue27.ref)(null);
5732
6686
  const baseUrl = `${options.adminEndpoint}/catalog/marketing-projections`;
5733
6687
  function authHeaders() {
5734
6688
  const token = options.getAuthToken?.();
@@ -5744,18 +6698,25 @@ function useMarketingProjections(options) {
5744
6698
  return `${baseUrl}?${params.toString()}`;
5745
6699
  }
5746
6700
  async function fetchJson(url, init) {
6701
+ const method = init?.method ?? "GET";
5747
6702
  const res = await http(url, {
5748
- method: init?.method ?? "GET",
6703
+ method,
5749
6704
  headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
5750
6705
  body: init?.body
5751
6706
  });
6707
+ requireServerAnswer(
6708
+ res.status,
6709
+ method,
6710
+ url,
6711
+ (diagnostic) => new MarketingProjectionsApiError(res.status, null, diagnostic)
6712
+ );
5752
6713
  if (res.status === 204) return null;
5753
6714
  const body = await res.json().catch(() => null);
5754
6715
  if (res.status >= 400) {
5755
6716
  throw new MarketingProjectionsApiError(
5756
6717
  res.status,
5757
6718
  body,
5758
- formatMessage(msg.value.errors.projectionsApi, { status: res.status })
6719
+ `Marketing projections API responded with HTTP ${res.status}`
5759
6720
  );
5760
6721
  }
5761
6722
  return body;
@@ -5782,7 +6743,9 @@ function useMarketingProjections(options) {
5782
6743
  body: JSON.stringify(data)
5783
6744
  });
5784
6745
  if (!created) {
5785
- throw new MarketingProjectionsApiError(0, null, "Create returned no body");
6746
+ throw markEmptyResponse(
6747
+ new MarketingProjectionsApiError(0, null, "Create returned no body")
6748
+ );
5786
6749
  }
5787
6750
  await load();
5788
6751
  return created;
@@ -5793,7 +6756,9 @@ function useMarketingProjections(options) {
5793
6756
  body: JSON.stringify(data)
5794
6757
  });
5795
6758
  if (!updated) {
5796
- throw new MarketingProjectionsApiError(0, null, "Update returned no body");
6759
+ throw markEmptyResponse(
6760
+ new MarketingProjectionsApiError(0, null, "Update returned no body")
6761
+ );
5797
6762
  }
5798
6763
  projections.value = projections.value.map((p) => p.id === id ? updated : p);
5799
6764
  return updated;
@@ -5809,13 +6774,14 @@ function useMarketingProjections(options) {
5809
6774
  }
5810
6775
 
5811
6776
  // src/vue/use-promotions.ts
5812
- var import_vue24 = require("vue");
6777
+ var import_vue28 = require("vue");
5813
6778
  var PromotionsApiError = class extends Error {
5814
6779
  constructor(status, body, message) {
5815
6780
  super(message);
5816
6781
  this.status = status;
5817
6782
  this.body = body;
5818
6783
  this.name = "PromotionsApiError";
6784
+ markPlatformError(this);
5819
6785
  }
5820
6786
  status;
5821
6787
  body;
@@ -5827,11 +6793,10 @@ function usePromotions(options) {
5827
6793
  if (!options?.projectKey) {
5828
6794
  throw new Error("usePromotions: `projectKey` is required.");
5829
6795
  }
5830
- const msg = useSaMessages("promos");
5831
6796
  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);
6797
+ const promotions = (0, import_vue28.ref)([]);
6798
+ const loading = (0, import_vue28.ref)(false);
6799
+ const error = (0, import_vue28.ref)(null);
5835
6800
  const baseUrl = `${options.adminEndpoint}/catalog/promotions`;
5836
6801
  const pk = encodeURIComponent(options.projectKey);
5837
6802
  function authHeaders() {
@@ -5839,18 +6804,25 @@ function usePromotions(options) {
5839
6804
  return token ? { Authorization: `Bearer ${token}` } : {};
5840
6805
  }
5841
6806
  async function fetchJson(url, init) {
6807
+ const method = init?.method ?? "GET";
5842
6808
  const res = await http(url, {
5843
- method: init?.method ?? "GET",
6809
+ method,
5844
6810
  headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
5845
6811
  body: init?.body
5846
6812
  });
6813
+ requireServerAnswer(
6814
+ res.status,
6815
+ method,
6816
+ url,
6817
+ (diagnostic) => new PromotionsApiError(res.status, null, diagnostic)
6818
+ );
5847
6819
  if (res.status === 204) return null;
5848
6820
  const body = await res.json().catch(() => null);
5849
6821
  if (res.status >= 400) {
5850
6822
  throw new PromotionsApiError(
5851
6823
  res.status,
5852
6824
  body,
5853
- formatMessage(msg.value.apiErrorHttpStatus, { status: res.status })
6825
+ `Promotions API responded with HTTP ${res.status}`
5854
6826
  );
5855
6827
  }
5856
6828
  return body;
@@ -5872,7 +6844,8 @@ function usePromotions(options) {
5872
6844
  method: "POST",
5873
6845
  body: JSON.stringify(data)
5874
6846
  });
5875
- if (!created) throw new PromotionsApiError(0, null, "Create returned no body");
6847
+ if (!created)
6848
+ throw markEmptyResponse(new PromotionsApiError(0, null, "Create returned no body"));
5876
6849
  promotions.value = [...promotions.value, created];
5877
6850
  return created;
5878
6851
  }
@@ -5881,7 +6854,8 @@ function usePromotions(options) {
5881
6854
  method: "PATCH",
5882
6855
  body: JSON.stringify(data)
5883
6856
  });
5884
- if (!updated) throw new PromotionsApiError(0, null, "Update returned no body");
6857
+ if (!updated)
6858
+ throw markEmptyResponse(new PromotionsApiError(0, null, "Update returned no body"));
5885
6859
  promotions.value = promotions.value.map((p) => p.id === id ? updated : p);
5886
6860
  return updated;
5887
6861
  }
@@ -5896,13 +6870,14 @@ function usePromotions(options) {
5896
6870
  }
5897
6871
 
5898
6872
  // src/vue/use-plans.ts
5899
- var import_vue25 = require("vue");
6873
+ var import_vue29 = require("vue");
5900
6874
  var PlansApiError = class extends Error {
5901
6875
  constructor(status, body, message) {
5902
6876
  super(message);
5903
6877
  this.status = status;
5904
6878
  this.body = body;
5905
6879
  this.name = "PlansApiError";
6880
+ markPlatformError(this);
5906
6881
  }
5907
6882
  status;
5908
6883
  body;
@@ -5917,18 +6892,19 @@ function usePlans(options) {
5917
6892
  throw new Error("usePlans: `projectKey` is required.");
5918
6893
  }
5919
6894
  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)({});
6895
+ const plans = (0, import_vue29.ref)([]);
6896
+ const loading = (0, import_vue29.ref)(false);
6897
+ const error = (0, import_vue29.ref)(null);
6898
+ const tenantCountsByPlanKey = (0, import_vue29.ref)({});
5924
6899
  const baseUrl = `${options.adminEndpoint}/catalog/plans`;
5925
6900
  function authHeaders() {
5926
6901
  const token = options.getAuthToken?.();
5927
6902
  return token ? { Authorization: `Bearer ${token}` } : {};
5928
6903
  }
5929
6904
  async function fetchJson(url, init) {
6905
+ const method = init?.method ?? "GET";
5930
6906
  const res = await http(url, {
5931
- method: init?.method ?? "GET",
6907
+ method,
5932
6908
  headers: {
5933
6909
  "content-type": "application/json",
5934
6910
  ...authHeaders(),
@@ -5936,6 +6912,12 @@ function usePlans(options) {
5936
6912
  },
5937
6913
  body: init?.body
5938
6914
  });
6915
+ requireServerAnswer(
6916
+ res.status,
6917
+ method,
6918
+ url,
6919
+ (diagnostic) => new PlansApiError(res.status, null, diagnostic)
6920
+ );
5939
6921
  if (res.status === 204) return null;
5940
6922
  const body = await res.json().catch(() => null);
5941
6923
  if (res.status >= 400) {
@@ -5976,7 +6958,8 @@ function usePlans(options) {
5976
6958
  method: "POST",
5977
6959
  body: JSON.stringify(data)
5978
6960
  });
5979
- if (!created) throw new PlansApiError(0, null, "Create returned no body");
6961
+ if (!created)
6962
+ throw markEmptyResponse(new PlansApiError(0, null, "Create returned no body"));
5980
6963
  plans.value = [...plans.value, created];
5981
6964
  return created;
5982
6965
  }
@@ -5985,7 +6968,8 @@ function usePlans(options) {
5985
6968
  method: "PATCH",
5986
6969
  body: JSON.stringify(data)
5987
6970
  });
5988
- if (!updated) throw new PlansApiError(0, null, "Update returned no body");
6971
+ if (!updated)
6972
+ throw markEmptyResponse(new PlansApiError(0, null, "Update returned no body"));
5989
6973
  plans.value = plans.value.map((p) => p.id === planId ? updated : p);
5990
6974
  return updated;
5991
6975
  }
@@ -6021,9 +7005,9 @@ function usePlanVersions(options) {
6021
7005
  throw new Error("usePlanVersions: `planId` is required.");
6022
7006
  }
6023
7007
  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);
7008
+ const versions = (0, import_vue29.ref)([]);
7009
+ const loading = (0, import_vue29.ref)(false);
7010
+ const error = (0, import_vue29.ref)(null);
6027
7011
  const planVersionsUrl = `${options.adminEndpoint}/catalog/plans/${options.planId}/versions`;
6028
7012
  const versionUrlBase = `${options.adminEndpoint}/catalog/plan-versions`;
6029
7013
  function authHeaders() {
@@ -6031,8 +7015,9 @@ function usePlanVersions(options) {
6031
7015
  return token ? { Authorization: `Bearer ${token}` } : {};
6032
7016
  }
6033
7017
  async function fetchJson(url, init) {
7018
+ const method = init?.method ?? "GET";
6034
7019
  const res = await http(url, {
6035
- method: init?.method ?? "GET",
7020
+ method,
6036
7021
  headers: {
6037
7022
  "content-type": "application/json",
6038
7023
  ...authHeaders(),
@@ -6040,6 +7025,12 @@ function usePlanVersions(options) {
6040
7025
  },
6041
7026
  body: init?.body
6042
7027
  });
7028
+ requireServerAnswer(
7029
+ res.status,
7030
+ method,
7031
+ url,
7032
+ (diagnostic) => new PlansApiError(res.status, null, diagnostic)
7033
+ );
6043
7034
  if (res.status === 204) return null;
6044
7035
  const body = await res.json().catch(() => null);
6045
7036
  if (res.status >= 400) {
@@ -6068,7 +7059,8 @@ function usePlanVersions(options) {
6068
7059
  method: "POST",
6069
7060
  body: JSON.stringify(data)
6070
7061
  });
6071
- if (!result) throw new PlansApiError(0, null, "CreateDraft returned no body");
7062
+ if (!result)
7063
+ throw markEmptyResponse(new PlansApiError(0, null, "CreateDraft returned no body"));
6072
7064
  versions.value = [...versions.value, result.planVersion];
6073
7065
  return result;
6074
7066
  }
@@ -6077,7 +7069,8 @@ function usePlanVersions(options) {
6077
7069
  `${versionUrlBase}/${versionId}`,
6078
7070
  { method: "PATCH", body: JSON.stringify(data) }
6079
7071
  );
6080
- if (!result) throw new PlansApiError(0, null, "UpdateDraft returned no body");
7072
+ if (!result)
7073
+ throw markEmptyResponse(new PlansApiError(0, null, "UpdateDraft returned no body"));
6081
7074
  versions.value = versions.value.map((v) => v.id === versionId ? result.planVersion : v);
6082
7075
  return result;
6083
7076
  }
@@ -6086,7 +7079,8 @@ function usePlanVersions(options) {
6086
7079
  `${versionUrlBase}/${versionId}/publish`,
6087
7080
  { method: "POST", body: JSON.stringify(opts) }
6088
7081
  );
6089
- if (!result) throw new PlansApiError(0, null, "Publish returned no body");
7082
+ if (!result)
7083
+ throw markEmptyResponse(new PlansApiError(0, null, "Publish returned no body"));
6090
7084
  versions.value = versions.value.map((v) => v.id === versionId ? result.planVersion : v);
6091
7085
  return result;
6092
7086
  }
@@ -6099,7 +7093,8 @@ function usePlanVersions(options) {
6099
7093
  `${versionUrlBase}/${versionId}/terminate`,
6100
7094
  { method: "POST", body: JSON.stringify({ endsAt }) }
6101
7095
  );
6102
- if (!updated) throw new PlansApiError(0, null, "Terminate returned no body");
7096
+ if (!updated)
7097
+ throw markEmptyResponse(new PlansApiError(0, null, "Terminate returned no body"));
6103
7098
  versions.value = versions.value.map((v) => v.id === versionId ? updated : v);
6104
7099
  return updated;
6105
7100
  }
@@ -6120,7 +7115,7 @@ function usePlanVersions(options) {
6120
7115
  }
6121
7116
 
6122
7117
  // src/vue/use-live-plan-versions.ts
6123
- var import_vue26 = require("vue");
7118
+ var import_vue30 = require("vue");
6124
7119
  function useLivePlanVersions(options) {
6125
7120
  if (!options?.adminEndpoint) {
6126
7121
  throw new Error("useLivePlanVersions: `adminEndpoint` is required.");
@@ -6129,9 +7124,9 @@ function useLivePlanVersions(options) {
6129
7124
  throw new Error("useLivePlanVersions: `plans` is required.");
6130
7125
  }
6131
7126
  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);
7127
+ const livePlanVersions = (0, import_vue30.ref)({});
7128
+ const loading = (0, import_vue30.ref)(false);
7129
+ const error = (0, import_vue30.ref)(null);
6135
7130
  function authHeaders() {
6136
7131
  const token = options.getAuthToken?.();
6137
7132
  return token ? { Authorization: `Bearer ${token}` } : {};
@@ -6173,7 +7168,7 @@ function useLivePlanVersions(options) {
6173
7168
  loading.value = false;
6174
7169
  }
6175
7170
  }
6176
- (0, import_vue26.watch)(
7171
+ (0, import_vue30.watch)(
6177
7172
  () => options.plans.value.map((p) => p.id).join(","),
6178
7173
  () => {
6179
7174
  if (options.plans.value.length > 0) void refresh();
@@ -6195,12 +7190,12 @@ function findLatestLive(versions) {
6195
7190
  }
6196
7191
 
6197
7192
  // src/vue/use-manifest.ts
6198
- var import_vue27 = require("vue");
7193
+ var import_vue31 = require("vue");
6199
7194
  function useManifest(options) {
6200
7195
  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);
7196
+ const manifest = (0, import_vue31.ref)(null);
7197
+ const loading = (0, import_vue31.ref)(false);
7198
+ const error = (0, import_vue31.ref)(null);
6204
7199
  async function load() {
6205
7200
  loading.value = true;
6206
7201
  error.value = null;
@@ -6226,24 +7221,24 @@ function useManifest(options) {
6226
7221
  }
6227
7222
 
6228
7223
  // src/vue/use-nav.ts
6229
- var import_vue28 = require("vue");
7224
+ var import_vue32 = require("vue");
6230
7225
  function useNav(manifest, options = {}) {
6231
7226
  const { locale } = useSuperAdminI18n();
6232
- const activeLocale = (0, import_vue28.computed)(() => options.locale ?? locale.value);
6233
- const routes = (0, import_vue28.computed)(() => {
7227
+ const activeLocale = (0, import_vue32.computed)(() => options.locale ?? locale.value);
7228
+ const routes = (0, import_vue32.computed)(() => {
6234
7229
  if (!manifest.value) return [];
6235
7230
  return buildRoutes(manifest.value, { ...options, locale: activeLocale.value });
6236
7231
  });
6237
- const sidebar = (0, import_vue28.computed)(
7232
+ const sidebar = (0, import_vue32.computed)(
6238
7233
  () => buildSidebar(routes.value, defaultSectionOrder(activeLocale.value))
6239
7234
  );
6240
7235
  return { routes, sidebar };
6241
7236
  }
6242
7237
 
6243
7238
  // src/vue/use-actions.ts
6244
- var import_vue29 = require("vue");
7239
+ var import_vue33 = require("vue");
6245
7240
  function useActions(manifest, actions) {
6246
- const registry = (0, import_vue29.computed)(() => {
7241
+ const registry = (0, import_vue33.computed)(() => {
6247
7242
  if (!manifest.value) return null;
6248
7243
  return new ActionRegistry(manifest.value, actions);
6249
7244
  });
@@ -6251,18 +7246,18 @@ function useActions(manifest, actions) {
6251
7246
  }
6252
7247
 
6253
7248
  // src/vue/use-tenant-action-flow.ts
6254
- var import_vue30 = require("vue");
7249
+ var import_vue34 = require("vue");
6255
7250
  function useTenantActionFlow(manifest, providers = {}) {
6256
7251
  const handlers = useSuperAdminActions();
6257
7252
  const msg = useSaMessages("tenants");
6258
- const registry = (0, import_vue30.computed)(() => {
7253
+ const registry = (0, import_vue34.computed)(() => {
6259
7254
  if (!manifest.value) return null;
6260
7255
  return new ActionRegistry(manifest.value, handlers);
6261
7256
  });
6262
- const orphanedDefs = (0, import_vue30.computed)(
7257
+ const orphanedDefs = (0, import_vue34.computed)(
6263
7258
  () => registry.value ? registry.value.listOrphanedDefs() : []
6264
7259
  );
6265
- const availableActions = (0, import_vue30.computed)(() => {
7260
+ const availableActions = (0, import_vue34.computed)(() => {
6266
7261
  const m = manifest.value;
6267
7262
  const reg = registry.value;
6268
7263
  if (!m || !reg) return [];
@@ -6335,7 +7330,7 @@ async function runFlow(def, row, registry, providers, successTemplate) {
6335
7330
  }
6336
7331
 
6337
7332
  // src/vue/use-platform-tenant-actions.ts
6338
- var import_vue31 = require("vue");
7333
+ var import_vue35 = require("vue");
6339
7334
  var DEFAULT_VISIBLE_FOR_ROW = (def, row) => {
6340
7335
  if (def.actionKey === "tenants.suspend") return row.isActive;
6341
7336
  if (def.actionKey === "tenants.reactivate") return !row.isActive;
@@ -6371,7 +7366,7 @@ function usePlatformTenantActions(options) {
6371
7366
  const iconForActionKey = options.iconForActionKey ?? DEFAULT_ICON_FOR_ACTION_KEY;
6372
7367
  const toneForActionKey = options.toneForActionKey ?? DEFAULT_TONE_FOR_ACTION_KEY;
6373
7368
  const visibleForRow = options.visibleForRow ?? DEFAULT_VISIBLE_FOR_ROW;
6374
- const mfa = (0, import_vue31.ref)({ show: false, description: "", error: "" });
7369
+ const mfa = (0, import_vue35.ref)({ show: false, description: "", error: "" });
6375
7370
  let pendingMfaResolve = null;
6376
7371
  function showMfaDialog(def, ctx) {
6377
7372
  return new Promise((resolve) => {
@@ -6396,7 +7391,7 @@ function usePlatformTenantActions(options) {
6396
7391
  pendingMfaResolve(null);
6397
7392
  }
6398
7393
  }
6399
- const confirmDialog = (0, import_vue31.ref)({
7394
+ const confirmDialog = (0, import_vue35.ref)({
6400
7395
  show: false,
6401
7396
  def: null,
6402
7397
  row: null
@@ -6438,7 +7433,7 @@ function usePlatformTenantActions(options) {
6438
7433
  },
6439
7434
  visibleForRow
6440
7435
  });
6441
- const manifestActions = (0, import_vue31.computed)(
7436
+ const manifestActions = (0, import_vue35.computed)(
6442
7437
  () => flow.availableActions.value.map((def) => ({
6443
7438
  id: def.id,
6444
7439
  label: def.label,
@@ -6452,7 +7447,7 @@ function usePlatformTenantActions(options) {
6452
7447
  }
6453
7448
  }))
6454
7449
  );
6455
- const realOrphans = (0, import_vue31.computed)(() => {
7450
+ const realOrphans = (0, import_vue35.computed)(() => {
6456
7451
  const caps = options.manifest.value?.capabilities ?? {};
6457
7452
  const defs = options.manifest.value?.tenants?.actions ?? [];
6458
7453
  const defByActionKey = new Map(defs.map((d) => [d.actionKey, d]));
@@ -6477,12 +7472,12 @@ function usePlatformTenantActions(options) {
6477
7472
  }
6478
7473
 
6479
7474
  // src/vue/use-batch-columns.ts
6480
- var import_vue32 = require("vue");
7475
+ var import_vue36 = require("vue");
6481
7476
  function useBatchColumns(manifest, tenantIds, options = {}) {
6482
7477
  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);
7478
+ const data = (0, import_vue36.ref)({});
7479
+ const loading = (0, import_vue36.ref)(false);
7480
+ const error = (0, import_vue36.ref)(null);
6486
7481
  async function load() {
6487
7482
  if (!manifest.value || tenantIds.value.length === 0) {
6488
7483
  data.value = {};
@@ -6499,22 +7494,23 @@ function useBatchColumns(manifest, tenantIds, options = {}) {
6499
7494
  loading.value = false;
6500
7495
  }
6501
7496
  }
6502
- (0, import_vue32.watch)([manifest, tenantIds], () => {
7497
+ (0, import_vue36.watch)([manifest, tenantIds], () => {
6503
7498
  void load();
6504
7499
  });
6505
7500
  return { data, loading, error, reload: load };
6506
7501
  }
6507
7502
 
6508
7503
  // src/vue/platform-loaders.ts
6509
- function resolveEndpoints(endpoints) {
7504
+ function resolveSuperAdminEndpoints(endpoints) {
6510
7505
  return {
6511
7506
  apiBase: endpoints.apiBase,
6512
7507
  publicBootEndpoint: endpoints.publicBootEndpoint ?? `${endpoints.apiBase}/boot`,
6513
- manifestEndpoint: endpoints.manifestEndpoint ?? `${endpoints.apiBase}/manifest`
7508
+ manifestEndpoint: endpoints.manifestEndpoint ?? `${endpoints.apiBase}/manifest`,
7509
+ projectKey: endpoints.projectKey ?? ""
6514
7510
  };
6515
7511
  }
6516
7512
  function createPlatformLoaders(options) {
6517
- const resolved = resolveEndpoints(options.endpoints);
7513
+ const resolved = resolveSuperAdminEndpoints(options.endpoints);
6518
7514
  const bootLoader = new BootLoader({
6519
7515
  endpoint: resolved.publicBootEndpoint,
6520
7516
  http: options.http
@@ -6531,13 +7527,13 @@ function createPlatformLoaders(options) {
6531
7527
 
6532
7528
  // src/vue/manifest-store-factory.ts
6533
7529
  var import_pinia = require("pinia");
6534
- var import_vue33 = require("vue");
7530
+ var import_vue37 = require("vue");
6535
7531
  function createManifestStore(options) {
6536
7532
  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);
7533
+ const manifest = (0, import_vue37.ref)(null);
7534
+ const loading = (0, import_vue37.ref)(false);
7535
+ const error = (0, import_vue37.ref)(null);
7536
+ const loaded = (0, import_vue37.ref)(false);
6541
7537
  let inflight = null;
6542
7538
  async function load() {
6543
7539
  loading.value = true;
@@ -6970,6 +7966,7 @@ function planChangeWizardI18n(i18n) {
6970
7966
  ALL_ROWS,
6971
7967
  ActionDefNotInManifestError,
6972
7968
  ActionRegistry,
7969
+ AdminError,
6973
7970
  BatchColumnDriftError,
6974
7971
  BatchColumnFetcher,
6975
7972
  BootLoadError,
@@ -6990,6 +7987,10 @@ function planChangeWizardI18n(i18n) {
6990
7987
  IDENTITY_ACCENT_VALUES,
6991
7988
  IDENTITY_NEUTRAL,
6992
7989
  IDENTITY_NEUTRAL_VALUE,
7990
+ LIST_FIRST_PAGE,
7991
+ LIST_PAGE_SIZE_DEFAULT,
7992
+ LIST_PAGE_SIZE_MAX,
7993
+ LIST_PAGINATION_PARAMS,
6993
7994
  ManifestLoadError,
6994
7995
  ManifestLoader,
6995
7996
  MarketingProjectionsApiError,
@@ -6999,15 +8000,18 @@ function planChangeWizardI18n(i18n) {
6999
8000
  PlansApiError,
7000
8001
  ProjectPageHost,
7001
8002
  PromotionsApiError,
8003
+ SA_COLOR_SCHEMES,
7002
8004
  SA_INTL_LOCALES,
7003
8005
  SA_LOCALES,
7004
8006
  SA_LOCALE_LABELS,
7005
8007
  SA_LOCALE_STORAGE_KEY,
7006
8008
  SA_MESSAGES,
7007
8009
  SA_THEME_KEY,
8010
+ SA_THEME_STORAGE_KEY,
7008
8011
  SERVER_PAGE_SIZE_OPTIONS,
7009
8012
  SUPER_ADMIN_ACTIONS_KEY,
7010
8013
  SUPER_ADMIN_BRAND_KEY,
8014
+ SUPER_ADMIN_CONFIRM_KEY,
7011
8015
  SUPER_ADMIN_ENDPOINTS_KEY,
7012
8016
  SUPER_ADMIN_EXTENSIONS_KEY,
7013
8017
  SUPER_ADMIN_HTTP_KEY,
@@ -7016,19 +8020,28 @@ function planChangeWizardI18n(i18n) {
7016
8020
  SUPER_ADMIN_MANIFEST_CLEAR_CACHE_KEY,
7017
8021
  SUPER_ADMIN_MANIFEST_KEY,
7018
8022
  SUPER_ADMIN_NOTIFY_KEY,
8023
+ SUPER_ADMIN_RESOURCES_KEY,
7019
8024
  TenantSubscriptionBundlesApiError,
8025
+ adminErrorMessage,
8026
+ auditResource,
8027
+ bindResource,
7020
8028
  buildFeatureRegistry,
7021
8029
  buildFeatureRouterGuard,
7022
8030
  buildNavigationGuard,
7023
8031
  buildQuotaRegistry,
7024
8032
  buildRoutes,
7025
8033
  buildSidebar,
8034
+ clampListPage,
8035
+ clampListPageSize,
7026
8036
  countPlans,
7027
8037
  createAdminResourceClient,
7028
8038
  createAdminRoutes,
8039
+ createAxiosHttpClient,
8040
+ createFetchHttpClient,
7029
8041
  createManifestStore,
7030
8042
  createPlatformLoaders,
7031
8043
  createProjectPageHostRoute,
8044
+ createResourceRegistry,
7032
8045
  createSaCatalog,
7033
8046
  createSaTheme,
7034
8047
  createSuperAdminI18n,
@@ -7038,30 +8051,57 @@ function planChangeWizardI18n(i18n) {
7038
8051
  defaultSectionOrder,
7039
8052
  defaultTenantPlanSectionI18n,
7040
8053
  defaultToneForActionKey,
8054
+ defineListOp,
7041
8055
  defineMessages,
8056
+ defineResource,
8057
+ filterQueryString,
7042
8058
  formatCurrency,
7043
8059
  formatMessage,
7044
8060
  getJson,
7045
8061
  identityAccentAt,
7046
8062
  identityAccentFor,
7047
8063
  identityChipStyle,
8064
+ isAdminError,
8065
+ isAxiosNoResponseError,
7048
8066
  isCurrentlyValid,
8067
+ isEmptyResponse,
7049
8068
  isExpired,
7050
8069
  isFutureScheduled,
8070
+ isPlatformError,
7051
8071
  isProductionBoot,
7052
8072
  isSaBuiltinLocale,
8073
+ isSentInQuery,
8074
+ isTransportFailure,
8075
+ listUrl,
8076
+ markEmptyResponse,
8077
+ markPlatformError,
8078
+ markTransportFailure,
7053
8079
  mergeMessages,
7054
8080
  planChangeWizardI18n,
8081
+ planVersionsResource,
8082
+ plansResource,
8083
+ platformResources,
7055
8084
  postJson,
7056
8085
  provideEntitlement,
8086
+ readErrorCode,
8087
+ readErrorDetail,
8088
+ readListPage,
8089
+ requestJson,
8090
+ requestJsonBody,
8091
+ requireServerAnswer,
7057
8092
  resolveExtension,
7058
8093
  resolveLoginBranding,
7059
8094
  resolveMessages,
7060
8095
  resolvePlans,
8096
+ resolveSuperAdminEndpoints,
8097
+ tenantsResource,
8098
+ toAdminError,
7061
8099
  todayIsoDate,
7062
8100
  trimTrailingSlashes,
7063
8101
  useActions,
7064
8102
  useApiList,
8103
+ useAsyncAction,
8104
+ useAsyncData,
7065
8105
  useAuditEntries,
7066
8106
  useBatchColumns,
7067
8107
  useBulkPublish,
@@ -7083,6 +8123,8 @@ function planChangeWizardI18n(i18n) {
7083
8123
  usePlatformTenantActions,
7084
8124
  usePromotions,
7085
8125
  usePublicBoot,
8126
+ useResource,
8127
+ useResourceList,
7086
8128
  useSaMessages,
7087
8129
  useSaTheme,
7088
8130
  useSubscriptionDraft,