@objectstack/rest 13.0.0 → 14.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -33,7 +33,14 @@ __export(index_exports, {
33
33
  RestServer: () => RestServer,
34
34
  RouteGroupBuilder: () => RouteGroupBuilder,
35
35
  RouteManager: () => RouteManager,
36
- createRestApiPlugin: () => createRestApiPlugin
36
+ buildFieldMetaMap: () => buildFieldMetaMap,
37
+ coerceRow: () => coerceRow,
38
+ createRestApiPlugin: () => createRestApiPlugin,
39
+ isMetaEnvelope: () => isMetaEnvelope,
40
+ parseCsvToRows: () => parseCsvToRows,
41
+ parseXlsxToRows: () => parseXlsxToRows,
42
+ prepareImportRequest: () => prepareImportRequest,
43
+ runImport: () => runImport
37
44
  });
38
45
  module.exports = __toCommonJS(index_exports);
39
46
 
@@ -327,6 +334,22 @@ function optionLabel(value, options) {
327
334
  const hit = options.find((o) => o && o.value === value);
328
335
  return hit?.label ?? value;
329
336
  }
337
+ function toArgb(color) {
338
+ if (typeof color !== "string") return void 0;
339
+ const hex = color.trim().replace(/^#/, "");
340
+ if (/^[0-9a-fA-F]{3}$/.test(hex)) {
341
+ const [r, g, b] = hex;
342
+ return `FF${r}${r}${g}${g}${b}${b}`.toUpperCase();
343
+ }
344
+ if (/^[0-9a-fA-F]{6}$/.test(hex)) return `FF${hex}`.toUpperCase();
345
+ return void 0;
346
+ }
347
+ function cellFontColor(value, meta) {
348
+ if (value === null || value === void 0) return void 0;
349
+ if (!meta || !meta.type || !OPTION_TYPES.has(meta.type) || !meta.options) return void 0;
350
+ const hit = meta.options.find((o) => o && o.value === value);
351
+ return toArgb(hit?.color);
352
+ }
330
353
  function displayFromRecord(rec, displayField) {
331
354
  if (displayField && rec[displayField] != null) return String(rec[displayField]);
332
355
  for (const k of NAME_KEY_FALLBACKS) {
@@ -913,155 +936,10 @@ function applyMappingToRows(rows, artifact) {
913
936
  return { ok: true, rows: out };
914
937
  }
915
938
 
916
- // src/rest-server.ts
917
- var import_meta = {};
918
- var logError = (...args) => globalThis.console?.error(...args);
919
- var TRANSLATABLE_META_TYPES = /* @__PURE__ */ new Set(["view", "action", "object", "app", "dashboard"]);
939
+ // src/import-prepare.ts
920
940
  function isMetaEnvelope(value) {
921
941
  return !!value && typeof value === "object" && typeof value.type === "string" && typeof value.name === "string" && value.item != null && typeof value.item === "object" && !Array.isArray(value.item);
922
942
  }
923
- function mapDataError(error, object) {
924
- if (error?.code === "DELETE_RESTRICTED") {
925
- return {
926
- status: 409,
927
- body: {
928
- error: error?.message ?? "Cannot delete: dependent records exist",
929
- code: "DELETE_RESTRICTED",
930
- ...error?.dependentObject ? { dependentObject: error.dependentObject } : {},
931
- ...typeof error?.dependentCount === "number" ? { dependentCount: error.dependentCount } : {},
932
- ...object ? { object } : {}
933
- }
934
- };
935
- }
936
- if (error?.code === "CONCURRENT_UPDATE" || error?.name === "ConcurrentUpdateError") {
937
- return {
938
- status: 409,
939
- body: {
940
- error: error?.message ?? "Record was modified by another user",
941
- code: "CONCURRENT_UPDATE",
942
- ...error?.currentVersion ? { currentVersion: error.currentVersion } : {},
943
- ...error?.currentRecord ? { currentRecord: error.currentRecord } : {},
944
- ...object ? { object } : {}
945
- }
946
- };
947
- }
948
- if (error?.code === "VALIDATION_FAILED" || error?.name === "ValidationError") {
949
- return {
950
- status: 400,
951
- body: {
952
- error: error?.message ?? "Validation failed",
953
- code: "VALIDATION_FAILED",
954
- fields: Array.isArray(error?.fields) ? error.fields : [],
955
- ...object ? { object } : {}
956
- }
957
- };
958
- }
959
- if (error?.code === "PERMISSION_DENIED" || error?.name === "PermissionDeniedError" || typeof error?.message === "string" && error.message.startsWith("[Security] Access denied")) {
960
- return {
961
- status: 403,
962
- body: {
963
- error: error?.message ?? "Permission denied",
964
- code: "PERMISSION_DENIED",
965
- ...object ? { object } : {}
966
- }
967
- };
968
- }
969
- const raw = String(error?.message ?? error ?? "");
970
- const lower = raw.toLowerCase();
971
- if (raw.includes("[EnvironmentKernelFactory]") && (lower.includes("missing database_url") || lower.includes("not found"))) {
972
- const isProvisioning = lower.includes("status='provisioning'") || lower.includes("status='pending'");
973
- const isFailed = lower.includes("status='failed'");
974
- return {
975
- status: isProvisioning ? 503 : isFailed ? 502 : 404,
976
- body: {
977
- error: raw,
978
- code: isProvisioning ? "PROJECT_PROVISIONING" : isFailed ? "PROJECT_PROVISIONING_FAILED" : "PROJECT_NOT_FOUND"
979
- }
980
- };
981
- }
982
- if (error?.code === "RECORD_NOT_FOUND" || /^Record\s+\S+\s+not found in\s+\S+/i.test(raw)) {
983
- return {
984
- status: 404,
985
- body: {
986
- error: raw,
987
- code: "RECORD_NOT_FOUND",
988
- ...object ? { object } : {}
989
- }
990
- };
991
- }
992
- const unknownColumn = /has no column named\s+["'`]?([a-z0-9_]+)/i.exec(raw) || /no such column:\s*["'`]?([a-z0-9_.]+)/i.exec(raw) || /unknown column\s+["'`]([a-z0-9_]+)["'`]/i.exec(raw) || /column\s+["'`]([a-z0-9_]+)["'`]\s+of relation\s+\S+\s+does not exist/i.exec(raw);
993
- if (unknownColumn) {
994
- const field = unknownColumn[1]?.split(".").pop();
995
- return {
996
- status: 400,
997
- body: {
998
- error: field ? `Unknown field '${field}'${object ? ` on object '${object}'` : ""}` : "Request references a field that does not exist",
999
- code: "INVALID_FIELD",
1000
- ...field ? { field } : {},
1001
- ...object ? { object } : {}
1002
- }
1003
- };
1004
- }
1005
- const notNull = /not null constraint failed:\s*\S*?\.([a-z0-9_]+)/i.exec(raw) || /null value in column\s+["'`]([a-z0-9_]+)["'`]/i.exec(raw) || /column\s+["'`]([a-z0-9_]+)["'`]\s+cannot be null/i.exec(raw);
1006
- if (notNull) {
1007
- const field = notNull[1];
1008
- return {
1009
- status: 400,
1010
- body: {
1011
- error: `${field} is required`,
1012
- code: "VALIDATION_FAILED",
1013
- fields: [{ field, code: "required", message: `${field} is required` }],
1014
- hint: `If '${field}' is optional in your object metadata, the database column is still NOT NULL \u2014 the physical schema has drifted from metadata. Run 'os migrate' to reconcile (or reset the dev database).`,
1015
- ...object ? { object } : {}
1016
- }
1017
- };
1018
- }
1019
- const looksLikeUnknownObject = lower.includes("no such table") || lower.includes("relation") && lower.includes("does not exist") || lower.includes("table not found") || lower.includes("unknown object") || lower.includes("object not found") || lower.includes("no driver available") || object !== void 0 && lower.includes(`'${object.toLowerCase()}'`) && lower.includes("not");
1020
- if (looksLikeUnknownObject) {
1021
- return {
1022
- status: 404,
1023
- body: {
1024
- error: object ? `Object '${object}' is not registered` : "Object not found",
1025
- code: "object_not_found",
1026
- object
1027
- }
1028
- };
1029
- }
1030
- const looksLikeSqlLeak = lower.includes("sqlite_") || lower.includes("sqlstate") || lower.startsWith("insert into ") || lower.startsWith("update ") || lower.startsWith("select ") || lower.startsWith("delete from ") || lower.includes("constraint failed") || lower.includes("unique constraint") || lower.includes("foreign key");
1031
- if (looksLikeSqlLeak) {
1032
- if (lower.includes("unique constraint") || lower.includes("unique violation")) {
1033
- return {
1034
- status: 409,
1035
- body: {
1036
- error: "A record with this value already exists",
1037
- code: "UNIQUE_VIOLATION",
1038
- ...object ? { object } : {}
1039
- }
1040
- };
1041
- }
1042
- return {
1043
- status: 500,
1044
- body: { error: "Internal data error", code: "DATABASE_ERROR" }
1045
- };
1046
- }
1047
- return { status: 400, body: { error: raw || "Bad request" } };
1048
- }
1049
- function sendError(res, error, object) {
1050
- if (typeof error?.status === "number" && error.status >= 400 && error.status < 600) {
1051
- const safeMsg = typeof error.message === "string" && error.message.length < 500 ? error.message : "Request failed";
1052
- res.status(error.status).json({
1053
- error: safeMsg,
1054
- ...error.code ? { code: error.code } : {},
1055
- ...Array.isArray(error.issues) ? { issues: error.issues } : {}
1056
- });
1057
- return;
1058
- }
1059
- const mapped = mapDataError(error, object);
1060
- res.status(mapped.status).json(mapped.body);
1061
- }
1062
- function isExpectedDataStatus(status) {
1063
- return status === 403 || status === 404 || status === 409 || status === 502 || status === 503;
1064
- }
1065
943
  function parseCsvToRows(csv, mapping = {}) {
1066
944
  const text = csv.replace(/^\uFEFF/, "");
1067
945
  const cells = [];
@@ -1280,6 +1158,163 @@ async function prepareImportRequest(body, opts) {
1280
1158
  }
1281
1159
  };
1282
1160
  }
1161
+
1162
+ // src/rest-server.ts
1163
+ var import_meta = {};
1164
+ var logError = (...args) => globalThis.console?.error(...args);
1165
+ var TRANSLATABLE_META_TYPES = /* @__PURE__ */ new Set(["view", "action", "object", "app", "dashboard"]);
1166
+ function mapDataError(error, object) {
1167
+ if (error?.code === "DELETE_RESTRICTED") {
1168
+ return {
1169
+ status: 409,
1170
+ body: {
1171
+ error: error?.message ?? "Cannot delete: dependent records exist",
1172
+ code: "DELETE_RESTRICTED",
1173
+ ...error?.dependentObject ? { dependentObject: error.dependentObject } : {},
1174
+ ...typeof error?.dependentCount === "number" ? { dependentCount: error.dependentCount } : {},
1175
+ ...object ? { object } : {}
1176
+ }
1177
+ };
1178
+ }
1179
+ if (error?.code === "CONCURRENT_UPDATE" || error?.name === "ConcurrentUpdateError") {
1180
+ return {
1181
+ status: 409,
1182
+ body: {
1183
+ error: error?.message ?? "Record was modified by another user",
1184
+ code: "CONCURRENT_UPDATE",
1185
+ ...error?.currentVersion ? { currentVersion: error.currentVersion } : {},
1186
+ ...error?.currentRecord ? { currentRecord: error.currentRecord } : {},
1187
+ ...object ? { object } : {}
1188
+ }
1189
+ };
1190
+ }
1191
+ if (error?.code === "VALIDATION_FAILED" || error?.name === "ValidationError") {
1192
+ return {
1193
+ status: 400,
1194
+ body: {
1195
+ error: error?.message ?? "Validation failed",
1196
+ code: "VALIDATION_FAILED",
1197
+ fields: Array.isArray(error?.fields) ? error.fields : [],
1198
+ ...object ? { object } : {}
1199
+ }
1200
+ };
1201
+ }
1202
+ if (error?.code === "FEEDS_DISABLED" || error?.code === "FILES_DISABLED") {
1203
+ return {
1204
+ status: 403,
1205
+ body: {
1206
+ error: error?.message ?? "This capability is disabled for the target object",
1207
+ code: error.code,
1208
+ ...error?.object || object ? { object: error?.object ?? object } : {}
1209
+ }
1210
+ };
1211
+ }
1212
+ if (error?.code === "PERMISSION_DENIED" || error?.name === "PermissionDeniedError" || typeof error?.message === "string" && error.message.startsWith("[Security] Access denied")) {
1213
+ return {
1214
+ status: 403,
1215
+ body: {
1216
+ error: error?.message ?? "Permission denied",
1217
+ code: "PERMISSION_DENIED",
1218
+ ...object ? { object } : {}
1219
+ }
1220
+ };
1221
+ }
1222
+ const raw = String(error?.message ?? error ?? "");
1223
+ const lower = raw.toLowerCase();
1224
+ if (raw.includes("[EnvironmentKernelFactory]") && (lower.includes("missing database_url") || lower.includes("not found"))) {
1225
+ const isProvisioning = lower.includes("status='provisioning'") || lower.includes("status='pending'");
1226
+ const isFailed = lower.includes("status='failed'");
1227
+ return {
1228
+ status: isProvisioning ? 503 : isFailed ? 502 : 404,
1229
+ body: {
1230
+ error: raw,
1231
+ code: isProvisioning ? "PROJECT_PROVISIONING" : isFailed ? "PROJECT_PROVISIONING_FAILED" : "PROJECT_NOT_FOUND"
1232
+ }
1233
+ };
1234
+ }
1235
+ if (error?.code === "RECORD_NOT_FOUND" || /^Record\s+\S+\s+not found in\s+\S+/i.test(raw)) {
1236
+ return {
1237
+ status: 404,
1238
+ body: {
1239
+ error: raw,
1240
+ code: "RECORD_NOT_FOUND",
1241
+ ...object ? { object } : {}
1242
+ }
1243
+ };
1244
+ }
1245
+ const unknownColumn = /has no column named\s+["'`]?([a-z0-9_]+)/i.exec(raw) || /no such column:\s*["'`]?([a-z0-9_.]+)/i.exec(raw) || /unknown column\s+["'`]([a-z0-9_]+)["'`]/i.exec(raw) || /column\s+["'`]([a-z0-9_]+)["'`]\s+of relation\s+\S+\s+does not exist/i.exec(raw);
1246
+ if (unknownColumn) {
1247
+ const field = unknownColumn[1]?.split(".").pop();
1248
+ return {
1249
+ status: 400,
1250
+ body: {
1251
+ error: field ? `Unknown field '${field}'${object ? ` on object '${object}'` : ""}` : "Request references a field that does not exist",
1252
+ code: "INVALID_FIELD",
1253
+ ...field ? { field } : {},
1254
+ ...object ? { object } : {}
1255
+ }
1256
+ };
1257
+ }
1258
+ const notNull = /not null constraint failed:\s*\S*?\.([a-z0-9_]+)/i.exec(raw) || /null value in column\s+["'`]([a-z0-9_]+)["'`]/i.exec(raw) || /column\s+["'`]([a-z0-9_]+)["'`]\s+cannot be null/i.exec(raw);
1259
+ if (notNull) {
1260
+ const field = notNull[1];
1261
+ return {
1262
+ status: 400,
1263
+ body: {
1264
+ error: `${field} is required`,
1265
+ code: "VALIDATION_FAILED",
1266
+ fields: [{ field, code: "required", message: `${field} is required` }],
1267
+ hint: `If '${field}' is optional in your object metadata, the database column is still NOT NULL \u2014 the physical schema has drifted from metadata. Run 'os migrate' to reconcile (or reset the dev database).`,
1268
+ ...object ? { object } : {}
1269
+ }
1270
+ };
1271
+ }
1272
+ const looksLikeUnknownObject = lower.includes("no such table") || lower.includes("relation") && lower.includes("does not exist") || lower.includes("table not found") || lower.includes("unknown object") || lower.includes("object not found") || lower.includes("no driver available") || object !== void 0 && lower.includes(`'${object.toLowerCase()}'`) && lower.includes("not");
1273
+ if (looksLikeUnknownObject) {
1274
+ return {
1275
+ status: 404,
1276
+ body: {
1277
+ error: object ? `Object '${object}' is not registered` : "Object not found",
1278
+ code: "object_not_found",
1279
+ object
1280
+ }
1281
+ };
1282
+ }
1283
+ const looksLikeSqlLeak = lower.includes("sqlite_") || lower.includes("sqlstate") || lower.startsWith("insert into ") || lower.startsWith("update ") || lower.startsWith("select ") || lower.startsWith("delete from ") || lower.includes("constraint failed") || lower.includes("unique constraint") || lower.includes("foreign key");
1284
+ if (looksLikeSqlLeak) {
1285
+ if (lower.includes("unique constraint") || lower.includes("unique violation")) {
1286
+ return {
1287
+ status: 409,
1288
+ body: {
1289
+ error: "A record with this value already exists",
1290
+ code: "UNIQUE_VIOLATION",
1291
+ ...object ? { object } : {}
1292
+ }
1293
+ };
1294
+ }
1295
+ return {
1296
+ status: 500,
1297
+ body: { error: "Internal data error", code: "DATABASE_ERROR" }
1298
+ };
1299
+ }
1300
+ return { status: 400, body: { error: raw || "Bad request" } };
1301
+ }
1302
+ function sendError(res, error, object) {
1303
+ if (typeof error?.status === "number" && error.status >= 400 && error.status < 600) {
1304
+ const safeMsg = typeof error.message === "string" && error.message.length < 500 ? error.message : "Request failed";
1305
+ res.status(error.status).json({
1306
+ error: safeMsg,
1307
+ ...error.code ? { code: error.code } : {},
1308
+ ...Array.isArray(error.issues) ? { issues: error.issues } : {}
1309
+ });
1310
+ return;
1311
+ }
1312
+ const mapped = mapDataError(error, object);
1313
+ res.status(mapped.status).json(mapped.body);
1314
+ }
1315
+ function isExpectedDataStatus(status) {
1316
+ return status === 403 || status === 404 || status === 409 || status === 502 || status === 503;
1317
+ }
1283
1318
  var IMPORT_JOB_OBJECT = "sys_import_job";
1284
1319
  var IMPORT_JOB_MAX_ROWS = 5e4;
1285
1320
  var IMPORT_JOB_RESULTS_CAP = 500;
@@ -1384,7 +1419,7 @@ function rowsToCsv(fields, rows, includeHeader, metaMap) {
1384
1419
  }
1385
1420
  return lines.join("\r\n") + (lines.length > 0 ? "\r\n" : "");
1386
1421
  }
1387
- async function createXlsxStream(res) {
1422
+ async function createXlsxStream(res, useStyles = false) {
1388
1423
  const { PassThrough } = await import("stream");
1389
1424
  const ExcelJS = (await import("exceljs")).default ?? await import("exceljs");
1390
1425
  const passthrough = new PassThrough();
@@ -1401,7 +1436,7 @@ async function createXlsxStream(res) {
1401
1436
  });
1402
1437
  passthrough.on("error", reject);
1403
1438
  });
1404
- const wb = new ExcelJS.stream.xlsx.WorkbookWriter({ stream: passthrough, useStyles: false });
1439
+ const wb = new ExcelJS.stream.xlsx.WorkbookWriter({ stream: passthrough, useStyles });
1405
1440
  const ws = wb.addWorksheet("Export");
1406
1441
  return {
1407
1442
  ws,
@@ -1412,8 +1447,8 @@ async function createXlsxStream(res) {
1412
1447
  }
1413
1448
  };
1414
1449
  }
1415
- var RestServer = class {
1416
- constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider) {
1450
+ var RestServer = class _RestServer {
1451
+ constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider, securityServiceProvider) {
1417
1452
  /**
1418
1453
  * Short-TTL cache for `hostname → environmentId` (P1-4). `resolveByHostname`
1419
1454
  * is a control-plane lookup (typically a DB query) that otherwise runs on
@@ -1466,6 +1501,7 @@ var RestServer = class {
1466
1501
  this.analyticsServiceProvider = analyticsServiceProvider;
1467
1502
  this.settingsServiceProvider = settingsServiceProvider;
1468
1503
  this.serviceExistsProvider = serviceExistsProvider;
1504
+ this.securityServiceProvider = securityServiceProvider;
1469
1505
  }
1470
1506
  /**
1471
1507
  * Resolve the protocol for a given request. When `environmentId` is present
@@ -1704,6 +1740,54 @@ var RestServer = class {
1704
1740
  perReq.set(key, pending);
1705
1741
  return pending;
1706
1742
  }
1743
+ /**
1744
+ * [ADR-0046 §6.7] The audience-evaluation view of the caller for book/doc
1745
+ * gating. `permissionSets` resolves through the security service's
1746
+ * `resolvePermissionSetNames` — the SAME resolution as data-plane
1747
+ * enforcement (positions expanded, additive baseline), so the docs gate
1748
+ * can never drift from it. `permissionSets` stays undefined when the
1749
+ * service is absent or resolution fails; `audienceAllows` then DENIES
1750
+ * permission-set-gated audiences (fail closed, ADR-0049). Resolution is
1751
+ * skipped unless `needPermissionSets` — callers pass true only when a
1752
+ * `{ permissionSet }` audience is actually in play.
1753
+ */
1754
+ async resolveAudienceCaller(environmentId, req, opts) {
1755
+ const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
1756
+ const authenticated = !!ctx?.userId;
1757
+ if (!authenticated || !opts.needPermissionSets || !this.securityServiceProvider) {
1758
+ return { authenticated };
1759
+ }
1760
+ try {
1761
+ const svc = await this.securityServiceProvider(environmentId);
1762
+ if (!svc || typeof svc.resolvePermissionSetNames !== "function") return { authenticated };
1763
+ const names = await svc.resolvePermissionSetNames(ctx);
1764
+ return { authenticated, permissionSets: Array.isArray(names) ? names : [] };
1765
+ } catch {
1766
+ return { authenticated };
1767
+ }
1768
+ }
1769
+ /** Whether any of these books carries a `{ permissionSet }` audience. */
1770
+ static anyPermissionSetAudience(books) {
1771
+ return books.some(
1772
+ (b) => b && typeof b === "object" && b.audience && typeof b.audience === "object" && typeof b.audience.permissionSet === "string"
1773
+ );
1774
+ }
1775
+ /** Coerce a getMetaItems result (array | {items}) into an array. */
1776
+ static metaItemsArray(raw) {
1777
+ if (Array.isArray(raw)) return raw;
1778
+ if (raw && typeof raw === "object" && Array.isArray(raw.items)) return raw.items;
1779
+ return [];
1780
+ }
1781
+ /** Fetch every book of the environment, shaped for the audience resolver. */
1782
+ async fetchAudienceBooks(p, environmentId) {
1783
+ const raw = await p.getMetaItems({
1784
+ type: "book",
1785
+ ...environmentId ? { environmentId } : {}
1786
+ }).catch(() => []);
1787
+ return _RestServer.metaItemsArray(raw).map(
1788
+ (b) => b && typeof b === "object" ? { ...b, packageId: b._packageId } : b
1789
+ );
1790
+ }
1707
1791
  /** Heavy path behind `resolveExecCtx` — resolve identity + RBAC/RLS + localization. */
1708
1792
  async computeExecCtx(environmentId, req) {
1709
1793
  try {
@@ -2212,6 +2296,8 @@ var RestServer = class {
2212
2296
  this.registerReportsEndpoints(bp);
2213
2297
  this.registerApprovalsEndpoints(bp);
2214
2298
  this.registerAnalyticsEndpoints(bp);
2299
+ this.registerSecurityEndpoints(bp);
2300
+ this.registerSecurityExplainEndpoints(bp);
2215
2301
  this.registerDataActionEndpoints(bp);
2216
2302
  if (this.config.api.enableCrud) {
2217
2303
  this.registerCrudEndpoints(bp);
@@ -2573,6 +2659,48 @@ var RestServer = class {
2573
2659
  visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
2574
2660
  }
2575
2661
  }
2662
+ if (req.params.type === "book") {
2663
+ const raw = visible;
2664
+ const list = _RestServer.metaItemsArray(raw);
2665
+ if (list.length > 0) {
2666
+ const { audienceAllows } = await import("@objectstack/spec/system");
2667
+ const caller = await this.resolveAudienceCaller(environmentId, req, {
2668
+ needPermissionSets: _RestServer.anyPermissionSetAudience(list)
2669
+ });
2670
+ const filtered = list.filter((b) => b && typeof b === "object" && audienceAllows(b.audience, caller));
2671
+ visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
2672
+ }
2673
+ }
2674
+ if (req.params.type === "doc") {
2675
+ const raw = visible;
2676
+ const list = _RestServer.metaItemsArray(raw);
2677
+ if (list.length > 0) {
2678
+ const { audienceAllows, docAudienceAllows, resolveDocAudiences } = await import("@objectstack/spec/system");
2679
+ const books = await this.fetchAudienceBooks(p, environmentId);
2680
+ const caller = await this.resolveAudienceCaller(environmentId, req, {
2681
+ needPermissionSets: _RestServer.anyPermissionSetAudience(books)
2682
+ });
2683
+ let filtered;
2684
+ if (caller.authenticated && !_RestServer.anyPermissionSetAudience(books)) {
2685
+ filtered = list;
2686
+ } else {
2687
+ const corpus = list.filter((d) => d && typeof d === "object").map((d) => ({
2688
+ name: d.name,
2689
+ group: d.group,
2690
+ tags: d.tags,
2691
+ order: d.order,
2692
+ packageId: d._packageId
2693
+ }));
2694
+ const audiences = resolveDocAudiences(books, corpus);
2695
+ filtered = list.filter((d) => {
2696
+ if (!d || typeof d !== "object") return false;
2697
+ const eff = audiences.get(d.name);
2698
+ return eff ? docAudienceAllows(eff, caller) : audienceAllows("org", caller);
2699
+ });
2700
+ }
2701
+ visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
2702
+ }
2703
+ }
2576
2704
  if (req.params.type === "doc") {
2577
2705
  const locale = this.extractLocale(req);
2578
2706
  const { resolveDocLocale } = await import("@objectstack/spec/system");
@@ -2646,7 +2774,7 @@ var RestServer = class {
2646
2774
  const prot = await this.resolveProtocol(environmentId, req);
2647
2775
  const locale = this.extractLocale(req);
2648
2776
  const packageId = req.query?.package || void 0;
2649
- const { resolveBookTree, deriveImplicitPackageBook, isPublicAudience, resolveDocLocale } = await import("@objectstack/spec/system");
2777
+ const { resolveBookTree, deriveImplicitPackageBook, audienceAllows, resolveDocAudiences, docAudienceAllows, resolveDocLocale } = await import("@objectstack/spec/system");
2650
2778
  const norm = (raw) => Array.isArray(raw) ? raw : raw && Array.isArray(raw.items) ? raw.items : [];
2651
2779
  const books = norm(await prot.getMetaItems({
2652
2780
  type: "book",
@@ -2657,9 +2785,16 @@ var RestServer = class {
2657
2785
  if (!book) {
2658
2786
  book = deriveImplicitPackageBook(req.params.name, req.params.name);
2659
2787
  }
2660
- const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
2661
- if (!ctx?.userId && !isPublicAudience(book.audience)) {
2662
- sendError(res, { code: "unauthorized", message: "This documentation requires sign-in", status: 401 });
2788
+ const audienceBooks = books.map((b) => b && typeof b === "object" ? { ...b, packageId: b._packageId } : b);
2789
+ const caller = await this.resolveAudienceCaller(environmentId, req, {
2790
+ needPermissionSets: _RestServer.anyPermissionSetAudience([book, ...audienceBooks])
2791
+ });
2792
+ if (!audienceAllows(book.audience, caller)) {
2793
+ if (!caller.authenticated) {
2794
+ sendError(res, { code: "unauthorized", message: "This documentation requires sign-in", status: 401 });
2795
+ } else {
2796
+ sendError(res, { code: "PERMISSION_DENIED", message: "This documentation is limited to holders of a permission set you do not have", status: 403 });
2797
+ }
2663
2798
  return;
2664
2799
  }
2665
2800
  const docs = norm(await prot.getMetaItems({
@@ -2676,6 +2811,14 @@ var RestServer = class {
2676
2811
  packageId: d._packageId
2677
2812
  }));
2678
2813
  const tree = resolveBookTree(book, docs, book._packageId);
2814
+ const gatedTreePossible = !caller.authenticated || _RestServer.anyPermissionSetAudience(audienceBooks);
2815
+ if (gatedTreePossible) {
2816
+ const audiences = resolveDocAudiences(audienceBooks, docs);
2817
+ tree.groups = tree.groups.map((g) => ({
2818
+ ...g,
2819
+ entries: g.entries.filter((e) => !e.doc || docAudienceAllows(audiences.get(e.doc), caller))
2820
+ })).filter((g) => g.entries.some((e) => e.doc || e.href));
2821
+ }
2679
2822
  res.json(tree);
2680
2823
  } catch (error) {
2681
2824
  logError("[REST] Unhandled error:", error);
@@ -2710,7 +2853,7 @@ var RestServer = class {
2710
2853
  const isDraftRead = typeof req.query?.state === "string" && req.query.state.toLowerCase() === "draft";
2711
2854
  const previewDrafts = typeof req.query?.preview === "string" && req.query.preview.toLowerCase() === "draft";
2712
2855
  const packageScoped = typeof req.query?.package === "string" && req.query.package.length > 0;
2713
- if (metadata.enableCache && p.getMetaItemCached && !isAppType && !isDraftRead && !previewDrafts && !packageScoped && req.params.type !== "doc") {
2856
+ if (metadata.enableCache && p.getMetaItemCached && !isAppType && !isDraftRead && !previewDrafts && !packageScoped && req.params.type !== "doc" && req.params.type !== "book") {
2714
2857
  const cacheRequest = {
2715
2858
  ifNoneMatch: req.headers["if-none-match"],
2716
2859
  ifModifiedSince: req.headers["if-modified-since"]
@@ -2779,6 +2922,47 @@ var RestServer = class {
2779
2922
  const serviceGate = registered ? (n) => registered.has(n) : void 0;
2780
2923
  if (serviceGate) visible = this.filterDashboardForUser(visible, serviceGate);
2781
2924
  }
2925
+ if ((req.params.type === "book" || req.params.type === "doc") && visible) {
2926
+ const { audienceAllows, docAudienceAllows, resolveDocAudiences } = await import("@objectstack/spec/system");
2927
+ const target = isMetaEnvelope(visible) ? visible.item : visible;
2928
+ let caller;
2929
+ let allowed;
2930
+ if (req.params.type === "book") {
2931
+ caller = await this.resolveAudienceCaller(environmentId, req, {
2932
+ needPermissionSets: _RestServer.anyPermissionSetAudience([target])
2933
+ });
2934
+ allowed = audienceAllows(target?.audience, caller);
2935
+ } else {
2936
+ const books = await this.fetchAudienceBooks(p, environmentId);
2937
+ caller = await this.resolveAudienceCaller(environmentId, req, {
2938
+ needPermissionSets: _RestServer.anyPermissionSetAudience(books)
2939
+ });
2940
+ if (caller.authenticated && !_RestServer.anyPermissionSetAudience(books)) {
2941
+ allowed = true;
2942
+ } else {
2943
+ const corpus = _RestServer.metaItemsArray(await p.getMetaItems({
2944
+ type: "doc",
2945
+ ...environmentId ? { environmentId } : {}
2946
+ }).catch(() => [])).filter((d) => d && typeof d === "object").map((d) => ({
2947
+ name: d.name,
2948
+ group: d.group,
2949
+ tags: d.tags,
2950
+ order: d.order,
2951
+ packageId: d._packageId
2952
+ }));
2953
+ const audiences = resolveDocAudiences(books, corpus);
2954
+ allowed = docAudienceAllows(audiences.get(target?.name), caller);
2955
+ }
2956
+ }
2957
+ if (!allowed) {
2958
+ if (!caller.authenticated) {
2959
+ sendError(res, { code: "unauthorized", message: "This documentation requires sign-in", status: 401 });
2960
+ } else {
2961
+ sendError(res, { code: "PERMISSION_DENIED", message: "This documentation is limited to holders of a permission set you do not have", status: 403 });
2962
+ }
2963
+ return;
2964
+ }
2965
+ }
2782
2966
  if (req.params.type === "doc" && visible) {
2783
2967
  const locale = this.extractLocale(req);
2784
2968
  const { resolveDocLocale } = await import("@objectstack/spec/system");
@@ -3779,9 +3963,11 @@ var RestServer = class {
3779
3963
  const includeHeader = String(q.header ?? "true").toLowerCase() !== "false";
3780
3964
  const HARD_CAP = 5e4;
3781
3965
  const MAX_CHUNK = 5e3;
3966
+ const STYLE_ROW_CAP = 1e4;
3782
3967
  const requestedLimit = q.limit != null ? Math.max(1, Number(q.limit) || 0) : 1e4;
3783
3968
  const limit = Math.min(requestedLimit, HARD_CAP);
3784
3969
  const chunkSize = Math.min(MAX_CHUNK, Math.max(50, q.page != null ? Number(q.page) || 500 : 500));
3970
+ const styled = format === "xlsx" && limit <= STYLE_ROW_CAP;
3785
3971
  let filter = void 0;
3786
3972
  if (typeof q.filter === "string" && q.filter.length > 0) {
3787
3973
  try {
@@ -3848,12 +4034,13 @@ var RestServer = class {
3848
4034
  }
3849
4035
  res.header("X-Export-Format", format);
3850
4036
  res.header("X-Export-Limit", String(limit));
4037
+ if (format === "xlsx") res.header("X-Export-Styles", styled ? "applied" : "dropped");
3851
4038
  res.header("Cache-Control", "no-store");
3852
4039
  let exported = 0;
3853
4040
  let firstChunk = true;
3854
4041
  let skip = 0;
3855
4042
  if (format === "json") res.write("[");
3856
- const xlsx = format === "xlsx" ? await createXlsxStream(res) : null;
4043
+ const xlsx = format === "xlsx" ? await createXlsxStream(res, styled) : null;
3857
4044
  while (exported < limit) {
3858
4045
  const take = Math.min(chunkSize, limit - exported);
3859
4046
  const findArgs = {
@@ -3881,8 +4068,16 @@ var RestServer = class {
3881
4068
  if (firstChunk && includeHeader) {
3882
4069
  xlsx.ws.addRow((fields ?? []).map((f) => headerLabel(f, metaMap))).commit();
3883
4070
  }
4071
+ const cols = fields ?? [];
3884
4072
  for (const row of rows) {
3885
- xlsx.ws.addRow(formatRowCells(row, fields ?? [], metaMap)).commit();
4073
+ const r = xlsx.ws.addRow(formatRowCells(row, cols, metaMap));
4074
+ if (styled) {
4075
+ cols.forEach((f, i) => {
4076
+ const argb = cellFontColor(row?.[f], metaMap.get(f));
4077
+ if (argb) r.getCell(i + 1).font = { color: { argb } };
4078
+ });
4079
+ }
4080
+ r.commit();
3886
4081
  }
3887
4082
  } else {
3888
4083
  for (let i = 0; i < rows.length; i++) {
@@ -4511,6 +4706,100 @@ var RestServer = class {
4511
4706
  metadata: { summary: "Run a semantic-layer dataset (preview/query)", tags: ["analytics"] }
4512
4707
  });
4513
4708
  }
4709
+ /**
4710
+ * [ADR-0090 D6] Access-explanation endpoint — the REST face of the
4711
+ * explain engine (framework#2696).
4712
+ *
4713
+ * GET {basePath}/security/explain?object=…&operation=…&userId=…
4714
+ * POST {basePath}/security/explain body: { object, operation, userId? }
4715
+ *
4716
+ * Delegates to the security service's `explain(request, callerContext)`
4717
+ * (`SecurityPlugin.explainAccessForCaller`) — the same code paths the
4718
+ * enforcement middleware runs, so the report is explained by
4719
+ * construction. Caller authorization lives in the SERVICE, not here:
4720
+ * explaining ANOTHER user requires `manage_users` or a delegated
4721
+ * `adminScope` covering that user (D12); the service's
4722
+ * `PermissionDeniedError` maps to 403. The route itself only insists on
4723
+ * an authenticated caller (an access report is sensitive even about
4724
+ * oneself, and the anonymous `guest` posture is not this endpoint's
4725
+ * business) and returns 501 when no security service exposing `explain`
4726
+ * is mounted (a deployment without `@objectstack/plugin-security`).
4727
+ */
4728
+ registerSecurityExplainEndpoints(basePath) {
4729
+ const isScoped = basePath.includes("/environments/:environmentId");
4730
+ const resolveService = async (environmentId, req) => {
4731
+ try {
4732
+ const envId = await this.resolveRequestEnvironmentId(environmentId, req);
4733
+ if (envId && envId !== "platform" && this.kernelManager) {
4734
+ const kernel = await this.kernelManager.getOrCreate(envId);
4735
+ const svc = await kernel.getServiceAsync("security").catch(() => void 0);
4736
+ if (svc) return svc;
4737
+ }
4738
+ } catch {
4739
+ }
4740
+ if (!this.securityServiceProvider) return void 0;
4741
+ try {
4742
+ return await this.securityServiceProvider(environmentId);
4743
+ } catch {
4744
+ return void 0;
4745
+ }
4746
+ };
4747
+ const handler = async (req, res) => {
4748
+ try {
4749
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
4750
+ const context = await this.resolveExecCtx(environmentId, req);
4751
+ if (this.enforceAuth(req, res, context)) return;
4752
+ if (!context?.userId) {
4753
+ return res.status(401).json({
4754
+ code: "UNAUTHORIZED",
4755
+ message: "The access-explanation endpoint requires an authenticated caller."
4756
+ });
4757
+ }
4758
+ const svc = await resolveService(environmentId, req);
4759
+ if (!svc || typeof svc.explain !== "function") {
4760
+ return res.status(501).json({
4761
+ code: "NOT_IMPLEMENTED",
4762
+ message: "Access explanation is not available on this deployment (no security service with explain)."
4763
+ });
4764
+ }
4765
+ const src = req.method === "GET" ? req.query ?? {} : req.body ?? {};
4766
+ const { ExplainRequestSchema } = await import("@objectstack/spec/security");
4767
+ const parsed = ExplainRequestSchema.safeParse({
4768
+ object: src.object,
4769
+ operation: src.operation ?? "read",
4770
+ ...src.userId != null && src.userId !== "" ? { userId: src.userId } : {}
4771
+ });
4772
+ if (!parsed.success) {
4773
+ return res.status(400).json({
4774
+ code: "VALIDATION_FAILED",
4775
+ message: "Invalid explain request \u2014 expected { object: string, operation: read|create|update|delete|transfer|restore|purge, userId?: string }.",
4776
+ detail: String(parsed.error?.message ?? "").slice(0, 1e3)
4777
+ });
4778
+ }
4779
+ const decision = await svc.explain(parsed.data, context);
4780
+ res.json(decision);
4781
+ } catch (error) {
4782
+ const msg = String(error?.message ?? error ?? "");
4783
+ if (error?.code === "PERMISSION_DENIED" || error?.name === "PermissionDeniedError" || msg.startsWith("[Security] Access denied")) {
4784
+ return res.status(403).json({ code: "PERMISSION_DENIED", message: msg.slice(0, 1e3) });
4785
+ }
4786
+ logError("[REST] Security explain error:", error);
4787
+ res.status(500).json({ code: "EXPLAIN_FAILED", error: msg.slice(0, 500) });
4788
+ }
4789
+ };
4790
+ this.routeManager.register({
4791
+ method: "GET",
4792
+ path: `${basePath}/security/explain`,
4793
+ handler,
4794
+ metadata: { summary: "Explain why a principal can (or cannot) perform an operation on an object (ADR-0090 D6)", tags: ["security"] }
4795
+ });
4796
+ this.routeManager.register({
4797
+ method: "POST",
4798
+ path: `${basePath}/security/explain`,
4799
+ handler,
4800
+ metadata: { summary: "Explain why a principal can (or cannot) perform an operation on an object (ADR-0090 D6)", tags: ["security"] }
4801
+ });
4802
+ }
4514
4803
  registerSharingEndpoints(basePath) {
4515
4804
  const { crud } = this.config;
4516
4805
  const dataPath = `${basePath}${crud.dataPrefix}`;
@@ -4753,6 +5042,106 @@ var RestServer = class {
4753
5042
  metadata: { summary: "Re-evaluate a sharing rule and reconcile grants", tags: ["sharing"] }
4754
5043
  });
4755
5044
  }
5045
+ /**
5046
+ * Register the security admin endpoints (ADR-0090 D5/D9) — suggested
5047
+ * audience bindings. A package permission set declaring `isDefault: true`
5048
+ * is an install-time SUGGESTION to bind it to the `everyone` position;
5049
+ * these routes surface pending suggestions and let a tenant admin resolve
5050
+ * them. The `security` service (plugin-security) does the real gating:
5051
+ * tenant-admin pre-check on all three, and confirm writes the binding
5052
+ * with the caller's execution context so the audience-anchor and
5053
+ * delegated-admin gates enforce it — never auto-bound, never system.
5054
+ *
5055
+ * GET {basePath}/security/suggested-bindings?status=&packageId=
5056
+ * POST {basePath}/security/suggested-bindings/:id/confirm
5057
+ * POST {basePath}/security/suggested-bindings/:id/dismiss
5058
+ *
5059
+ * Routes return 501 when the `security` service is not registered
5060
+ * (deployment without plugin-security). Typed service errors carry their
5061
+ * HTTP status (403 permission / 404 not found / 409 state).
5062
+ */
5063
+ registerSecurityEndpoints(basePath) {
5064
+ const dataPath = basePath;
5065
+ const isScoped = basePath.includes("/environments/:environmentId");
5066
+ const resolveService = async (environmentId) => {
5067
+ if (!this.securityServiceProvider) return void 0;
5068
+ try {
5069
+ const svc = await this.securityServiceProvider(environmentId);
5070
+ return svc && typeof svc.listAudienceBindingSuggestions === "function" ? svc : void 0;
5071
+ } catch {
5072
+ return void 0;
5073
+ }
5074
+ };
5075
+ const respond501 = (res) => res.status(501).json({
5076
+ code: "NOT_IMPLEMENTED",
5077
+ message: "Security service is not configured on this deployment"
5078
+ });
5079
+ const handleError = (err, res, defaultCode) => {
5080
+ const status = typeof err?.statusCode === "number" ? err.statusCode : 500;
5081
+ if (status !== 500) {
5082
+ return res.status(status).json({ code: err?.code ?? defaultCode, error: String(err?.message ?? err) });
5083
+ }
5084
+ logError(`[REST] suggested-bindings ${defaultCode}:`, err);
5085
+ return res.status(500).json({ code: defaultCode, error: String(err?.message ?? err).slice(0, 500) });
5086
+ };
5087
+ this.routeManager.register({
5088
+ method: "GET",
5089
+ path: `${dataPath}/security/suggested-bindings`,
5090
+ handler: async (req, res) => {
5091
+ try {
5092
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
5093
+ const context = await this.resolveExecCtx(environmentId, req);
5094
+ if (this.enforceAuth(req, res, context)) return;
5095
+ const svc = await resolveService(environmentId);
5096
+ if (!svc) return respond501(res);
5097
+ const result = await svc.listAudienceBindingSuggestions(context ?? {}, {
5098
+ status: req.query?.status ? String(req.query.status) : void 0,
5099
+ packageId: req.query?.packageId ? String(req.query.packageId) : void 0
5100
+ });
5101
+ res.json({ data: result });
5102
+ } catch (err) {
5103
+ handleError(err, res, "SUGGESTION_LIST_FAILED");
5104
+ }
5105
+ },
5106
+ metadata: { summary: "List suggested audience bindings (ADR-0090 D5/D9)", tags: ["security"] }
5107
+ });
5108
+ this.routeManager.register({
5109
+ method: "POST",
5110
+ path: `${dataPath}/security/suggested-bindings/:id/confirm`,
5111
+ handler: async (req, res) => {
5112
+ try {
5113
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
5114
+ const context = await this.resolveExecCtx(environmentId, req);
5115
+ if (this.enforceAuth(req, res, context)) return;
5116
+ const svc = await resolveService(environmentId);
5117
+ if (!svc) return respond501(res);
5118
+ const result = await svc.confirmAudienceBindingSuggestion(context ?? {}, String(req.params.id));
5119
+ res.json({ data: result });
5120
+ } catch (err) {
5121
+ handleError(err, res, "SUGGESTION_CONFIRM_FAILED");
5122
+ }
5123
+ },
5124
+ metadata: { summary: "Confirm a suggested audience binding (creates the everyone/guest binding)", tags: ["security"] }
5125
+ });
5126
+ this.routeManager.register({
5127
+ method: "POST",
5128
+ path: `${dataPath}/security/suggested-bindings/:id/dismiss`,
5129
+ handler: async (req, res) => {
5130
+ try {
5131
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
5132
+ const context = await this.resolveExecCtx(environmentId, req);
5133
+ if (this.enforceAuth(req, res, context)) return;
5134
+ const svc = await resolveService(environmentId);
5135
+ if (!svc) return respond501(res);
5136
+ const result = await svc.dismissAudienceBindingSuggestion(context ?? {}, String(req.params.id));
5137
+ res.json({ data: result });
5138
+ } catch (err) {
5139
+ handleError(err, res, "SUGGESTION_DISMISS_FAILED");
5140
+ }
5141
+ },
5142
+ metadata: { summary: "Dismiss a suggested audience binding", tags: ["security"] }
5143
+ });
5144
+ }
4756
5145
  /**
4757
5146
  * Register saved-report + scheduled-digest endpoints (M11.C16).
4758
5147
  *
@@ -5550,6 +5939,20 @@ function registerPackageRoutes(server, packageService, basePath = "/api/v1", opt
5550
5939
  try {
5551
5940
  const packageId = req.params.id;
5552
5941
  const version = req.query?.version;
5942
+ if (!version && typeof options.protocol?.deletePackage === "function") {
5943
+ const result2 = await options.protocol.deletePackage({ packageId });
5944
+ if (result2.failedCount === 0) {
5945
+ res.json({
5946
+ success: true,
5947
+ message: `Deleted ${packageId}`,
5948
+ deletedCount: result2.deletedCount,
5949
+ cleanups: result2.cleanups
5950
+ });
5951
+ return;
5952
+ }
5953
+ res.status(400).json({ success: false, failed: result2.failed, cleanups: result2.cleanups });
5954
+ return;
5955
+ }
5553
5956
  const result = await packageService.delete(packageId, version);
5554
5957
  if (result.success) {
5555
5958
  res.json({
@@ -5749,6 +6152,13 @@ function createRestApiPlugin(config = {}) {
5749
6152
  return void 0;
5750
6153
  }
5751
6154
  };
6155
+ const securityServiceProvider = async (_environmentId) => {
6156
+ try {
6157
+ return ctx.getService("security");
6158
+ } catch {
6159
+ return void 0;
6160
+ }
6161
+ };
5752
6162
  if (!server) {
5753
6163
  ctx.logger.warn(`RestApiPlugin: HTTP Server service '${serverService}' not found. REST routes skipped.`);
5754
6164
  return;
@@ -5766,7 +6176,7 @@ function createRestApiPlugin(config = {}) {
5766
6176
  }
5767
6177
  };
5768
6178
  try {
5769
- const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider);
6179
+ const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider, securityServiceProvider);
5770
6180
  restServer.registerRoutes();
5771
6181
  ctx.logger.info("REST API successfully registered");
5772
6182
  if (config.api?.api?.requireAuth === false) {
@@ -5822,6 +6232,13 @@ function createRestApiPlugin(config = {}) {
5822
6232
  RestServer,
5823
6233
  RouteGroupBuilder,
5824
6234
  RouteManager,
5825
- createRestApiPlugin
6235
+ buildFieldMetaMap,
6236
+ coerceRow,
6237
+ createRestApiPlugin,
6238
+ isMetaEnvelope,
6239
+ parseCsvToRows,
6240
+ parseXlsxToRows,
6241
+ prepareImportRequest,
6242
+ runImport
5826
6243
  });
5827
6244
  //# sourceMappingURL=index.cjs.map